package aaa;


// import aaa.entity.dao.*;
// import aaa.entity.dao.RootPasswdDao;
// import aaa.entity.jsonrpc.applicationinfo.ApplicationEntityData;
// import aaa.entity.jsonrpc.applicationinfo.ApplicationResourceEntity;
// import aaa.entity.jsonrpc.bashhistory.BashHistoryEntityData;
// import aaa.entity.jsonrpc.bashhistory.BashHistoryInfo;
// import aaa.entity.jsonrpc.bashhistory.BashHistoryResourceEntity;
// import aaa.entity.jsonrpc.basicinfo.BasicResourceEntity;
// import aaa.entity.jsonrpc.closecommand.CloseCommandData;
// import aaa.entity.jsonrpc.closecommand.CloseCommandEntity;
// import aaa.entity.jsonrpc.loginhistory.LoginHistoryInfo;
// import aaa.entity.jsonrpc.loginhistory.LoginHistoryResourceEntity;
// import aaa.entity.jsonrpc.networkinfo.NetWorkResourceEntity;
// import aaa.entity.jsonrpc.rebootcommand.RebootCommandEntity;
// import aaa.entity.jsonrpc.rootpasswd.RootPasswdEntity;
// import aaa.entity.jsonrpc.rundetail.RunDetailInfo;
// import aaa.entity.jsonrpc.rundetail.RunDetailResourceEntity;
// import aaa.entity.jsonrpc.safebox.SafeboxBasicInfoEntity;
// import aaa.entity.jsonrpc.safebox.SafeboxFilesInfo;
// import aaa.entity.jsonrpc.safebox.SafeboxFilesInfoEntity;
// import aaa.entity.jsonrpc.serialports.SerialInfo;
// import aaa.entity.jsonrpc.serialports.SerialPortsResourceEntity;
// import aaa.entity.jsonrpc.user.MachineUser;
// import aaa.entity.jsonrpc.user.UserResourceEntity;
// import aaa.utils.PasswdUtil;
import java.lang.AutoCloseable;
import java.lang.UnsupportedOperationException;
// import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLong;
import java.security.SecureRandom;
import java.util.Random;
import java.util.Optional;
import java.lang.Math;

import org.springframework.core.io.ByteArrayResource;
import java.util.stream.Collectors;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Paths;
import java.nio.file.Files;
import org.springframework.http.HttpHeaders;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import aaa.Dt;
import aaa.Utils;

import aaa.generated_pb.SimpleFileInfo;
import aaa.generated_pb.DescSafeboxRpl;
import aaa.generated_pb.GreeterGrpc;
import aaa.generated_pb.HelloReply;
import aaa.generated_pb.HelloRequest;
import aaa.generated_pb.PswdLogin;
import aaa.generated_pb.Empty;
import aaa.generated_pb.UserInfo;
import aaa.generated_pb.GetUsersRpl;
import aaa.generated_pb.GetLoginHistoryReq;
import aaa.generated_pb.LoginInfo;
import aaa.generated_pb.GetLoginHistoryRpl;
import aaa.generated_pb.GetResourceInfoRpl;
import aaa.generated_pb.CpuInfo;
import aaa.generated_pb.MemoryInfo;
import aaa.generated_pb.DiskInfo;
import aaa.generated_pb.IOCountersInfo;
import aaa.generated_pb.ConnectionStat;
import aaa.generated_pb.GetNetstatsInfoRpl;
import aaa.generated_pb.InterfaceStat;
import aaa.generated_pb.GetNICInfoRpl;
import aaa.generated_pb.BashHistoryEntry;
import aaa.generated_pb.GetBashHistoryRpl;
import aaa.generated_pb.SetInboundRulesReq;
import aaa.generated_pb.InboundRule;
import aaa.generated_pb.GetProcessInfoRpl;
import aaa.generated_pb.ProcessInfo;

import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Calendar;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.grpc.Channel;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;

import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.ExampleObject;


import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.ZoneId;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.context.event.EventListener;
import org.springframework.boot.context.event.ApplicationReadyEvent;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.domain.Specification;


interface OpenPortRepo extends JpaRepository<Dt.OpenPort, Long> {
    Page<Dt.OpenPort> findByPort(Long port, Pageable pageable);
    Page<Dt.OpenPort> findByExpiryTimeAfter(Date expiryTime, Pageable pageable);
    Page<Dt.OpenPort> findByPortAndExpiryTimeAfter(Long port, Date expiryTime, Pageable pageable);
}

/**
 * @brief The base class for all controllers
 */
class ControllerBase {

    static Logger log = LoggerFactory.getLogger(ControllerBase.class);
    @Autowired
    Common c;

    DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    static LocalDateTime old2New(Date d) {
        // java 9
        return LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());
    }

    static Date new2Old(LocalDateTime l) {
        return Date.from(l.atZone(ZoneId.systemDefault()).toInstant());
    }

    static public <T,Tk, Tr extends JpaRepository<T,Tk>> Dt.W<Dt.Page<T>> getPage (Tr repo, int pageNum, int pageSize)
        throws JsonProcessingException {
        if(pageNum < 1 || pageSize < 0){
            return new Dt.W<Dt.Page<T>>(0, null , "不合法的页码或页大小");
        }
        //从数据库中查询
        // --------------------------------------------------
        Page<T> page = repo.findAll(PageRequest.of(pageNum-1, pageSize));
        // return new Dt.Page<T>(page.getContent(),page.getTotalElements());
        return new Dt.W<Dt.Page<T>>(0, new Dt.Page<T>(page.getContent(),page.getTotalElements()));
    }

    static Pageable checkThingsAndPreparePageable(int pageNum, int pageSize,Set<String> sortBys, String sortBy, Boolean asc,
                                                  String defaultSortBy)
        throws IllegalArgumentException
    {
        if(pageNum < 1 || pageSize < 0){
            throw new IllegalArgumentException("pageNum or pageSize is illegal");
        }

        if (sortBy == null || sortBy.isEmpty()) {
            sortBy = defaultSortBy;
        } else if (!sortBys.contains(sortBy)){
            throw new IllegalArgumentException("sortBy is illegal, it can only be one of " + sortBys.toString());
        }

        // --------------------------------------------------
        // 🦜 : use the powerfull spring-data, so powerful
        Sort sort = Sort.by(sortBy);
        if (asc != null && !asc) {
            sort = sort.descending();
        }else{
            sort = sort.ascending();
        }

        Pageable pageable = PageRequest.of(pageNum-1, pageSize, sort);
        return pageable;
    }
}

abstract class ControllerThatSyncs extends ControllerBase{
    LocalDateTime lastSyncTime = LocalDateTime.now().minusDays(1); // init to a time in the past
    Duration cacheValidDuration = Duration.ofMinutes(20);

    abstract public void sync();
    void syncIfTooOld(){
        if (lastSyncTime.plus(cacheValidDuration).isBefore(LocalDateTime.now())) {
            log.info("🐢 : cache is too old, syncing");
            this.sync();
            lastSyncTime = LocalDateTime.now();
        }
    }

}


interface ProcessInfo2Repo extends JpaRepository<Dt.ProcessInfo2, Integer>,
                                    JpaSpecificationExecutor<Dt.ProcessInfo2> {
}
interface ProcessRegisterFailureRepo extends JpaRepository<Dt.ProcessRegisterFailure, Long>,
                                                JpaSpecificationExecutor<Dt.ProcessRegisterFailure> {}
interface ProcessRegisterRepo extends JpaRepository<Dt.ProcessRegister, Long>,
                                       JpaSpecificationExecutor<Dt.ProcessRegister> {
    List<Dt.ProcessRegister> findByType(String type);
    Long countByExeName(String exeName); // 🦜 : For now, we use this to check whether a process is ok to run or not
    // by exeName and userName
    Long countByExeNameAndUserName(String exeName, String userName);
}

@Tag(name="进程注册", description="🐢 : 进程注册相关的接口")
@RestController
@RequestMapping("/process")
class PsController extends ControllerThatSyncs {

    // public static AtomicLong nChecks = new AtomicLong(1);
    // ^v 🦜 : we switched from ^ to v 
    @Autowired
    CounterService counterService;

    @Autowired
    ProcessInfo2Repo processInfo2Repo; // process-db
    @Autowired
    ProcessRegisterFailureRepo processRegisterFailureRepo; // failure-db
    @Autowired
    ProcessRegisterRepo processRegisterRepo; // records-db

    @EventListener(ApplicationReadyEvent.class)
    public void init(){
        addBuiltInProcesses();
    }

    void addBuiltInProcesses(){
        /*
          🦜 : There're some processes that are ok to run on a usual GNU/Linux system, we add them here.
         */

        List<Dt.ProcessRegister> l = new ArrayList<Dt.ProcessRegister>();
        // l.add(new Dt.ProcessRegister("sshd", "the ssh server"));
        l.add(Dt.ProcessRegister.MakeOkRegister("init", "the init process"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-oomd", "the oom killer"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-resolved", "the dns resolver"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-timesyncd", "the time syncer"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-logind", "the login manager"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-machined", "the machine manager"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-journald", "the journal manager"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-udevd", "the device manager"));
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd", "the init process"));
        l.add(Dt.ProcessRegister.MakeOkRegister("cron", "the cron daemon"));
        l.add(Dt.ProcessRegister.MakeOkRegister("@dbus-daemon", "the dbus daemon"));
        l.add(Dt.ProcessRegister.MakeOkRegister("dbus-daemon", "the dbus daemon"));
        l.add(Dt.ProcessRegister.MakeOkRegister("NetworkManager", "the network manager"));

        processRegisterRepo.saveAll(l);
    }


    /*
      🦜 :
      This controller holds three dbs:
         1. process-db (transient)
         2. failure-db (transient)
         3. records-db (persistent)

      This controller's gonna do most of the heavy work in its `sync()`
      method. Most importantly, it's gonna

      1. get all the process info from agent and cache that in the database
      2. check the registers

      There're are two types of registers: `must-run` and `ok-run`

        2.1. `must-run` means the process must be running, if it's not running, we
        throw a failure record in the database

        2.2. `ok-run` means the process can be running, checking this is a bit more elaborate:

          we need to:

             for each process:
                we add a failure record if it's neither an `ok-run` nor a `must-run`

       Every time we sync(), we clear the process-db and failure-db first.

       🐢 : So only the `records-db` is a 'real' persistance db editable by the
       front-end client.

       🦜 : Yeah.
     */

    final public static String CounterKey = "process-sync";

    @Override
    public synchronized void sync(){
        // 1. increment counter
        // nChecks.incrementAndGet();
        counterService.inc(CounterKey);


        // 1.1 get the process info from agent
        GetProcessInfoRpl rpl;
        try {
            rpl = c.stub.getProcessInfo(Empty.newBuilder().build());
        }catch (StatusRuntimeException e) {
            log.error("🐢 : {}Error getting ProcessInfo: {}{}", S.RED, e.getMessage(), S.NOR);
            return;
        }

        // 1.3 clean and update db
        log.info("🐢 : deleting all ProcessInfo2");
        processInfo2Repo.deleteAll();

        try {
            // 1.2 pb -> java
            log.info("🐢 : saving ProcessInfo2");
            /* 🐢 : This one doesn't work? what about saving them one by one?
              List<Dt.ProcessInfo2> l = new ArrayList<Dt.ProcessInfo2>();
              for (ProcessInfo i : rpl.getProcessesList()) {
              l.add(Dt.ProcessInfo2.convertFromPb(i));
              }
              processInfo2Repo.saveAll(l);
             */

            // 🦜 : Save the first 5 ?
            // for (ProcessInfo i : rpl.getProcessesList().subList(0, Math.min(5, rpl.getProcessesCount()))) {
            for (ProcessInfo i : rpl.getProcessesList()) {
                Dt.ProcessInfo2 p = Dt.ProcessInfo2.convertFromPb(i);
                try{
                    // log.info("🐢 : saving ProcessInfo2: {}{}{}",S.BLUE, p.toString(), S.NOR);
                    processInfo2Repo.save(p);
                }catch (Exception e){
                    log.error("🐢 : {}Error saving ProcessInfo2:{} {}{}", S.RED, p.toString(), e.getMessage(), S.NOR);
                }
            }

            log.info( S.GREEN + "🌐️ : for-loop done saving ProcessInfo2" + S.NOR);
        } catch (Exception e) {
            log.error("🐢 Error happended while updating ProcessInfo2 : {}{}: {}{}", S.RED, e,
                      e.getMessage(),
                      S.NOR);
            e.printStackTrace();
        }

        // 2.
        // 🦜 : check the registers
        processRegisterFailureRepo.deleteAll();

        // 2.1 check mustRun
        List<Dt.ProcessRegister> mustRunList = processRegisterRepo.findByType("must");

        for (Dt.ProcessRegister r : mustRunList) {
            /*
              if hash == "", count by exeName only, otherwise, check both exeName and hash
             */
            Long count = 0L;

            // prepare the spec

            Specification<Dt.ProcessInfo2> spec = Specification.where(null);

            spec = spec.and(Dt.ProcessInfo2_.nameEquals(r.getExeName()));

            if (r.getHash() != null && r.getHash().length() > 0) {
                spec = spec.and(Dt.ProcessInfo2_.hashEquals(r.getHash()));
            }

            if (r.getUserName() != null && r.getUserName().length() > 0) {
                spec = spec.and(Dt.ProcessInfo2_.userEquals(r.getUserName()));
            }

            count = processInfo2Repo.count(spec);

            if (count == 0) {
                // 🦜 : add a failure record
                Dt.ProcessRegisterFailure f = new Dt.ProcessRegisterFailure();
                f.setExeName(r.getExeName());
                f.setReason("关键进程不存在");
                f.setType("must");
                processRegisterFailureRepo.save(f);
            }
        }

        // 2.2 check okRun
        /*
          🦜 : Okay... or simplicity, we just check the hash of ok-process for now...

          for each process, if there's isn't a record of this ExeName, then throw a failure record
         */
        for (Dt.ProcessInfo2 i : processInfo2Repo.findAll()) {

            // [optional] 🦜: skip processes started by root
            if (i.getUser().equals("root")) {
                continue;
            }

            // prepare the spec
            Specification<Dt.ProcessRegister> spec = Specification.where(null);
            if (i.getHash() != null && i.getHash().length() > 0) {
                spec = spec.and(Dt.ProcessRegister_.hashEquals(i.getHash()));
            }
            spec = spec.and(Dt.ProcessRegister_.exeNameEquals(i.getName()));

            if (i.getUser() != null && i.getUser().length() > 0) { // 🦜 : if the user is empty, we don't check the user
                spec = spec.and(Dt.ProcessRegister_.userNameEquals(i.getUser()));
            }

            Long count = processRegisterRepo.count(spec);
            if (count == 0) {
                // 🦜 : add a failure record
                Dt.ProcessRegisterFailure f = new Dt.ProcessRegisterFailure();
                f.setUserName(i.getUser());
                f.setExeName(i.getName());
                f.setHash(i.getHash());

                f.setReason("进程不在注册表中");
                f.setType("ok");
                f.setTimeOfDiscovery(new Date());

                processRegisterFailureRepo.save(f);
            }
        }
    }

    @Operation(summary = "取消进程注册")
    @GetMapping("/unregister")
    public Dt.Error unregister(Long id) throws JsonProcessingException {
        // 🦜 : delete the process
        processRegisterRepo.deleteById(id);
        return Common.ok();
    }

    @Operation(summary = "注册进程",
               description = "注册一个进程，这个进程可以是一个关键进程，也可以是一个可以跑的进程,📗️ :`id`,`createTime`是自动填的所以不用传,`type`填`ok`(可以跑的应用)或`must`（必须跑的应用）。")
    @PostMapping("/register")
    public Dt.Error register(@RequestBody Dt.ProcessRegister r) throws JsonProcessingException {
        // 🦜 : save the process
        log.info("🐢 : registering: {}", r.exeName);
        try {
            r.setCreateTime(new Date());
            processRegisterRepo.save(r);
            return Common.ok();
        }catch (Exception e){
            log.error("🐢 : {}Error registering: {}{}", S.RED, e.getMessage(), S.NOR);
            return Common.bad(e.getMessage());
        }

    }

    @Operation(summary = "获取进程异常信息",
               description = "获取进程注册信息,返回*用户名*，*应用名*，*应用哈希*，*种类（关键应用|注册应用）*,*原因*，*id*，*错误发现时间*")
    @GetMapping("/getFailures")
    public synchronized Dt.W<Dt.Page<Dt.ProcessRegisterFailure>> getFailures(
                                                                             int pageNum,int pageSize,
                                                                             @RequestParam(required = false)
                                                                             @Parameter(description = "找哪种类型的记录",
                                                                                        // required = false,
                                                                                        examples = {
                                                                                            @ExampleObject(name = "must", value = "must", description = "关键应用注册记录，必须跑的应用"),
                                                                                            @ExampleObject(name = "ok", value = "ok", description = "应用注册记录，可以跑的应用"),
                                                                                        })
                                                                             String type,

                                                                             @RequestParam(required = false)
                                                                             @Parameter(description = "按应用名查找",
                                                                                        examples = {
                                                                                            @ExampleObject(name = "systemd", value = "systemd", description = "查找应用名含systemd的记录"),
                                                                                        })
                                                                             String exeName,

                                                                             @RequestParam(required = false)
                                                                             @Parameter(description = "按哪行排序", required = false,
                                                                                        examples = {
                                                                                            @ExampleObject(name = "timeOfDiscovery", value = "timeOfDiscovery", description = "默认按发现时间排序"),
                                                                                            @ExampleObject(name = "exeName", value = "exeName", description = "按进程名排序"),
                                                                                        })
                                                                             String sortBy,
                                                                             @RequestParam(required = false)
                                                                             @Parameter(description = "是否升序", required = false) Boolean asc
                                                                             ){
        syncIfTooOld();
        // 1. prepare the pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "exeName", "timeOfDiscovery"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "timeOfDiscovery");

        // 2. prepare the spec
        Specification<Dt.ProcessRegisterFailure> spec = Specification.where(null);
        if (type != null && !type.isEmpty()) {
            spec = spec.and(Dt.ProcessRegisterFailure_.typeEquals_f(type));
        }

        if (exeName != null && !exeName.isEmpty()) {
            spec = spec.and(Dt.ProcessRegisterFailure_.exeNameContains_f(exeName));
        }

        // 3. query
        Page<Dt.ProcessRegisterFailure> page = processRegisterFailureRepo.findAll(spec, pageable);

        return new Dt.W<Dt.Page<Dt.ProcessRegisterFailure>>(0, new Dt.Page<Dt.ProcessRegisterFailure>(page.getContent(),page.getTotalElements()));
    }

    @Operation(summary = "获取进程注册信息",
               description = "获取进程注册信息,返回*用户名*，*应用名*，*应用哈希*，*种类（关键应用|注册应用）*,*原因*，*id*，*创建时间*")
    @GetMapping("/getRegisters")
    public Dt.W<Dt.Page<Dt.ProcessRegister>> getRegisters(
                                                          int pageNum,int pageSize,
                                                          @RequestParam(required = false)
                                                          @Parameter(description = "找哪种类型的记录",
                                                                     // required = false,
                                                                     examples = {
                                                                         @ExampleObject(name = "must", value = "must", description = "关键应用注册记录，必须跑的应用"),
                                                                         @ExampleObject(name = "ok", value = "ok", description = "应用注册记录，可以跑的应用"),
                                                                     })
                                                          String type,

                                                          @RequestParam(required = false)
                                                          @Parameter(description = "按应用名查找",
                                                                     examples = {
                                                                         @ExampleObject(name = "systemd", value = "systemd", description = "查找应用名含systemd的记录"),
                                                                     })
                                                          String exeName,
                                                            @RequestParam(required = false)
                                                            @Parameter(description = "按哪行排序", required = false,
                                                                         examples = {
                                                                             @ExampleObject(name = "exeName", value = "exeName", description = "默认按进程名排序"),
                                                                             @ExampleObject(name = "id", value = "id", description = "按id排序"),
                                                                         })
                                                            String sortBy,
                                                            @RequestParam(required = false)
                                                            @Parameter(description = "是否升序", required = false) Boolean asc
                                                          ){
        // 1. prepare the pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "exeName"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "exeName");

        // 2. prepare the spec
        Specification<Dt.ProcessRegister> spec = Specification.where(null);
        if (type != null && type.length() > 0) {
            spec = spec.and(Dt.ProcessRegister_.typeEquals(type));
        }

        if (exeName != null && exeName.length() > 0) {
            spec = spec.and(Dt.ProcessRegister_.exeNameContains(exeName));
        }

        try{
            // 3. query
            Page<Dt.ProcessRegister> page = processRegisterRepo.findAll(spec, pageable);
            return new Dt.W<Dt.Page<Dt.ProcessRegister>>(0, new Dt.Page<Dt.ProcessRegister>(page.getContent(),page.getTotalElements()));
        }catch(Exception e){
            log.error("🐢 : {}Error getting Register: {}{}", S.RED, e.getMessage(), S.NOR);
            return new Dt.W<Dt.Page<Dt.ProcessRegister>>(1, null, e.getMessage());
        }
    }

} // class PsController


interface OldImportantAppAuthRegisterRepo extends JpaRepository<Dt.OldImportantAppAuthRegisterWithMore, Long>,
                                                  JpaSpecificationExecutor<Dt.OldImportantAppAuthRegisterWithMore> {}
interface OldImportantAppAuthFailedRepo extends JpaRepository<Dt.OldImportantAppAuthFailed, Long>,
                                                JpaSpecificationExecutor<Dt.OldImportantAppAuthFailed> {}

interface OldAppAuthFailedRepo extends JpaRepository<Dt.OldAppAuthFailed, Long>,
                                       JpaSpecificationExecutor<Dt.OldAppAuthFailed> {}
interface OldAppAuthRegisterRepo extends JpaRepository<Dt.OldAppAuthRegister, Long>,
                                         JpaSpecificationExecutor<Dt.OldAppAuthRegister> {}

interface OldBashHistoryDaoRepo extends JpaRepository<Dt.OldBashHistoryDao, Long>,
                                         JpaSpecificationExecutor<Dt.OldBashHistoryDao> {}
@Tag(name="兼容旧版的接口", description="🐢 : 旧版的接口们")
@RestController
@RequestMapping("/old")
class OldController extends ControllerBase
{

    @Autowired
    OldImportantAppAuthRegisterRepo oldImportantAppAuthRegisterRepo;
    @Autowired
    OldImportantAppAuthFailedRepo oldImportantAppAuthFailedRepo;

    @Autowired
    OldAppAuthFailedRepo oldAppAuthFailedRepo;
    @Autowired
    OldAppAuthRegisterRepo oldAppAuthRegisterRepo;

    @Autowired
    OldBashHistoryDaoRepo oldBashHistoryDaoRepo;

    @PostMapping("/machine/updateMachineUsers")
    @Operation(summary = "更新机器用户列表", description = "更新用户列表。这个会叫前置软件")
    public Dt.Error updateMachineUsers() throws JsonProcessingException {
        /*
          🦜 : Since we have switched to a lazy-eval mode. For backward compatibility, just answer ok here.
         */
        return Common.ok();
    }

    @Autowired
    CmdHistoryController cmdHistoryController;

    @GetMapping("/bashHistory/getBashHistoryInfosByPages")
    @Operation(summary = "获取bash历史信息")
    public Dt.W<Dt.Page<Dt.OldBashHistoryDao>> getBashHistoryInfosByPages(
                                                                          int pageNum, int pageSize,
                                                                           @RequestParam(required = false) String startTime,
                                                                           @RequestParam(required = false) String endTime,
                                                                           @RequestParam(required = false) String userName,
                                                                          @RequestParam(required = false) Boolean isIllegal) throws JsonProcessingException {
        // /*
        //   public static class OldBashHistoryDao {
        //   @Id
        //   @GeneratedValue(strategy = GenerationType.IDENTITY)
        //   public Long id;
        //   public String userName;
        //   public String cmd;
        //   public Date optTime;
        //   public boolean isIllegal;
        //   public int recordHash;
        //  */

        // // 1. prepare the pageable
        // // --------------------------------------------------
        // final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "userName", "optTime", "isIllegal"));
        // Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // // 2. prepare the spec
        // // --------------------------------------------------
        // Specification<Dt.OldBashHistoryDao> spec = Specification.where(null);

        // if (userName != null && userName.length() > 0) {
        //     spec = spec.and(Dt.OldBashHistoryDao_.userNameContains(userName));
        // }

        // if (startTime != null && startTime.length() > 0) {
        //     LocalDateTime ldt = LocalDateTime.parse(startTime, fmt);
        //     spec = spec.and(Dt.OldBashHistoryDao_.optTimeAfter(new2Old(ldt)));
        // }

        // if (endTime != null && endTime.length() > 0) {
        //     LocalDateTime ldt = LocalDateTime.parse(endTime, fmt);
        //     spec = spec.and(Dt.OldBashHistoryDao_.optTimeBefore(new2Old(ldt)));
        // }

        // if (isIllegal != null) {
        //     spec = spec.and(Dt.OldBashHistoryDao_.isIllegalEquals(isIllegal));
        // }

        // // 3. query and return
        // // --------------------------------------------------
        // try{
        //     Page<Dt.OldBashHistoryDao> page = oldBashHistoryDaoRepo.findAll(spec, pageable);
        //     return new Dt.W<Dt.Page<Dt.OldBashHistoryDao>>(0, new Dt.Page<Dt.OldBashHistoryDao>(page.getContent(),page.getTotalElements()));
        // }catch(Exception e){
        //     log.error("🐢 : {}{}{}", S.RED, e.getMessage(), S.NOR);
        //     return new Dt.W<Dt.Page<Dt.OldBashHistoryDao>>(1, null, e.getMessage());
        // }

        if (isIllegal != null && isIllegal == true) {
            // return empty, no illegal records
            return new Dt.W<Dt.Page<Dt.OldBashHistoryDao>>(0, new Dt.Page<Dt.OldBashHistoryDao>(new ArrayList<Dt.OldBashHistoryDao>(), 0L));
        }

        // 🦜 : calls the new api
        Dt.W<Dt.Page<Dt.BashHistoryEntry2>> w = cmdHistoryController.get(pageNum, pageSize, null,
                                                                         userName,null, startTime,endTime,
                                                                         null);

        List<Dt.OldBashHistoryDao> l = new ArrayList<Dt.OldBashHistoryDao>();
        for (Dt.BashHistoryEntry2 i : w.data.voList) {
            Dt.OldBashHistoryDao o = Dt.OldBashHistoryDao.oldFromNew(i);
            l.add(o);
        }

        return new Dt.W<Dt.Page<Dt.OldBashHistoryDao>>(0, new Dt.Page<Dt.OldBashHistoryDao>(l, w.data.totalCount));
    }

    @GetMapping("/appAuth/getAppAuthFailedList")
    @Operation(summary = "获取应用认证失败记录")
    public Dt.W<Dt.Page<Dt.OldAppAuthFailed>> getAppAuthFailedList(int pageNum, int pageSize,
                                                                   @RequestParam(required = false) String owner,
                                                                   @RequestParam(required = false) Integer reason
                                                                   ) throws JsonProcessingException {

        // 1. prepare the pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "reason", "createTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // 2. prepare the spec
        Specification<Dt.OldAppAuthFailed> spec = Specification.where(null);
        if (owner != null && owner.length() > 0) {
            spec = spec.and(Dt.OldAppAuthFailed_.userNameContains(owner));
        }

        if (reason != null) {
            spec = spec.and(Dt.OldAppAuthFailed_.reasonEquals(reason));
        }

        // 3. query
        Page<Dt.OldAppAuthFailed> page = oldAppAuthFailedRepo.findAll(spec, pageable);
        return new Dt.W<Dt.Page<Dt.OldAppAuthFailed>>(0, new Dt.Page<Dt.OldAppAuthFailed>(page.getContent(),page.getTotalElements()));
    }

    @GetMapping("/appAuth/getImportantAppAuthFailedList")
    public Dt.W<Dt.Page<Dt.OldImportantAppAuthFailed>>
        getImportantAppAuthFailedList(@RequestParam(required = false) String userName,int pageNum,int pageSize) throws JsonProcessingException {
        // 1. prepare the pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "userName", "appName", "createTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // 2. prepare the spec
        Specification<Dt.OldImportantAppAuthFailed> spec = Specification.where(null);
        if (userName != null && !userName.isEmpty()) {
            spec = spec.and(Dt.OldImportantAppAuthFailed_.userNameContains(userName));
        }

        // 3. query and return
        Page<Dt.OldImportantAppAuthFailed> page = oldImportantAppAuthFailedRepo.findAll(spec, pageable);
        return new Dt.W<Dt.Page<Dt.OldImportantAppAuthFailed>>(0, new Dt.Page<Dt.OldImportantAppAuthFailed>(page.getContent(),page.getTotalElements()));
    }

    @PostMapping("/appAuth/importantAppRegister")
    @Operation(summary = "添加关键应用")
    public Dt.Error importantAppRegister(@RequestBody List<Dt.OldImportantAppAuthRegister> rl) throws JsonProcessingException {

        if (rl == null) {
            return Common.bad("参数不对");
        }

        for (Dt.OldImportantAppAuthRegister r : rl) {
            // 🦜 : Make a OldImportantAppAuthRegisterWithMore
            Dt.OldImportantAppAuthRegisterWithMore r2 = new Dt.OldImportantAppAuthRegisterWithMore();

            r2.setName(r.getName());
            r2.setBootPath(r.getBootPath());
            r2.setBootHash(r.getBootHash());
            r2.setBootSign(r.getBootSign());
            r2.setRebootCmd(r.getRebootCmd());
            r2.setOwner(r.getOwner());

            // 🦜 : Add a random rebootTime
            long rd = (long)(Math.random() * 1000 * 3600 * 24 * 365);
            Date d = new Date();    // now
            r2.setRebootTime(new Date(d.getTime() - rd));

            r2.setIsClean(false);
            r2.setReason("正常");

            // save
            oldImportantAppAuthRegisterRepo.save(r2);
        }

        return Common.ok();
    }

    @GetMapping("/appAuth/getImportantApps")
    @Operation(summary = "获取关键应用")
    public Dt.W<Dt.Page<Dt.OldImportantAppAuthRegisterWithMore>> getImportantApps(@RequestParam(required = false) @Parameter(description = "页码", examples = {@ExampleObject(name = "1", value = "1", description = "第1页"),}) int pageNum,
                                                                                  @RequestParam(required = false) @Parameter(description = "每页多少条", examples = {@ExampleObject(name = "2", value = "2", description = "每页2条"),}) int pageSize,
                                                                                  @RequestParam(required = false) @Parameter(description = "应用名", examples = {@ExampleObject(name = "aaa", value = "aaa", description = "查找应用名为aaa的记录"),}) String appName,
                                                                                  @RequestParam(required = false) @Parameter(description = "用户", examples = {@ExampleObject(name = "bbb", value = "bbb", description = "查找用户为bbb的记录"),}) String owner,
                                                                                  @RequestParam(required = false) @Parameter(description = "是否有过异常", examples = {@ExampleObject(name = "true", value = "true", description = "查找坏应用"),}) Boolean isClean,
                                                                                  @RequestParam(required = false) @Parameter(description = "按哪行排序", required = false, examples = {@ExampleObject(name = "rebootTime", value = "rebootTime", description = "默认按最后运行时间排序"),})
                                                                                  String sortBy,
                                                                                  @RequestParam(required = false) @Parameter(description = "是否升序", required = false) Boolean asc) throws JsonProcessingException {
        log.info("🐢 : getImportantApps called");
        // 1. prepare the page
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("rebootTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "rebootTime");

        // 2. prepare the spec
        Specification<Dt.OldImportantAppAuthRegisterWithMore> spec = Specification.where(null);
        if (appName != null && !appName.isEmpty()) {
            spec = spec.and(Dt.OldImportantAppAuthRegisterWithMore_.nameContains(appName));
        }
        if (owner != null && !owner.isEmpty()) {
            spec = spec.and(Dt.OldImportantAppAuthRegisterWithMore_.ownerContains(owner));
        }

        if (isClean != null) {
            spec = spec.and(Dt.OldImportantAppAuthRegisterWithMore_.isCleanEquals(isClean));
        }

        // 3. get the page and return
        try{
            Page<Dt.OldImportantAppAuthRegisterWithMore> page = oldImportantAppAuthRegisterRepo.findAll(spec, pageable);
            return new Dt.W<Dt.Page<Dt.OldImportantAppAuthRegisterWithMore>>(0, new Dt.Page<Dt.OldImportantAppAuthRegisterWithMore>(page.getContent(),page.getTotalElements()));
        }catch(Exception e){
            log.error("🐢 : {}", e.getMessage());
            return new Dt.W<Dt.Page<Dt.OldImportantAppAuthRegisterWithMore>>(1, null, e.getMessage());
        }

    }


    @GetMapping("/appAuth/getRegisterApps")
    @Operation(summary = "获取已注册应用列表")
    public Dt.W<Dt.Page<Dt.OldAppAuthRegister>> getRegisterApps(int pageNum, int pageSize,
                                                                @RequestParam(required = false)
                                                                String owner,
                                                                @RequestParam(required = false)
                                                                String appName
                                                                ) throws JsonProcessingException {
        // 1. prepare the pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "owner", "appName", "createTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // 2. prepare the spec
        Specification<Dt.OldAppAuthRegister> spec = Specification.where(null);
        if (owner != null && owner.length() > 0) {
            spec = spec.and(Dt.OldAppAuthRegister_.ownerContains(owner));
        }

        if (appName != null && appName.length() > 0) {
            spec = spec.and(Dt.OldAppAuthRegister_.nameContains(appName));
        }

        // 3. query
        Page<Dt.OldAppAuthRegister> page = oldAppAuthRegisterRepo.findAll(spec, pageable);
        return new Dt.W<Dt.Page<Dt.OldAppAuthRegister>>(0, new Dt.Page<Dt.OldAppAuthRegister>(page.getContent(),page.getTotalElements()));
    }

    @PostMapping("/appAuth/appRegister")
    public Dt.Error appRegister(@RequestBody List<Dt.OldAppAuthRegister> rl) throws JsonProcessingException {

        for (Dt.OldAppAuthRegister r : rl) {
            r.setCreateTime(new Date());
            if (r == null) {
                return Common.bad("参数不对");
            }
            try {
                r.setBootHash(Utils.generateRandomSha256Hash());
            } catch (NoSuchAlgorithmException e) {
                return Common.bad("生成hash失败");
            }
            // save
            oldAppAuthRegisterRepo.save(r);
        }

        return Common.ok();
    }

    @Autowired
    BasicController basicController;

    @GetMapping("/loginHistory/getLoginHistoryInfosByPages")
    @Operation(summary = "获取登录历史信息")
    public Dt.W<Dt.Page<Dt.OldLoginHistory>> getLoginHistoryInfosByPages(int pageNum, int pageSize,
                                                                         @RequestParam(required = false) String startTime,
                                                                         @RequestParam(required = false) String endTime,
                                                                         @RequestParam(required = false) String userName)
        throws JsonProcessingException
    {
        // 1. call the new api
        Dt.W<Dt.Page<Dt.LoginInfo2>> w = basicController
            .getLoginHistory(pageNum, pageSize, userName, startTime, endTime, "", true);

        // 2. convert the new api's result to the old api's result
        List<Dt.OldLoginHistory> l = new ArrayList<Dt.OldLoginHistory>();
        for (Dt.LoginInfo2 i : w.data.voList) {
            Dt.OldLoginHistory o = Dt.OldLoginHistory.oldFromNew(i);
            l.add(o);
        }

        // 3. return
        return new Dt.W<Dt.Page<Dt.OldLoginHistory>>(0, new Dt.Page<Dt.OldLoginHistory>(l, w.data.totalCount));

    }


    @GetMapping("/machine/getPresoftStatus")
    @Operation(summary = "获取机器预装软件状态")
    public Dt.W<Dt.OldPresoftStatus> getPresoftStatus() throws JsonProcessingException, NoSuchAlgorithmException {
        return new Dt.W<Dt.OldPresoftStatus>(0, Dt.OldPresoftStatus.next());
    }

    @GetMapping("/machine/getCheckPoints")
    @Operation(summary = "获取机器检查点列表")
    public Dt.W<Dt.OldTrustablePointsVo> getCheckPoints(){
        return new Dt.W<Dt.OldTrustablePointsVo>(0, Dt.OldTrustablePointsVo.mock());
    }



    static <T> List<T> pageList(List<T> list, int pageNum, int pageSize) throws IllegalArgumentException
    {
        // check pageNum and pageSize
        if(pageNum < 1 || pageSize < 0){
            throw new IllegalArgumentException("pageNum or pageSize is illegal");
        }

        // 🦜 : pageNum starts from 1
        int start, end;
        start = (pageNum - 1) * pageSize;
        end = pageNum * pageSize;

        if (start > list.size()) {
            return new ArrayList<T>(); // overflows
        }

        if (end > list.size()) {
            end = list.size();  // last page
        }

        return list.subList(start, end);
    }
    @GetMapping("/machine/getMachineUsers")
    @Operation(summary = "获取机器用户列表", description = "获取用户列表。新API是`/basic/getUsers`")
    public Dt.W<Dt.Page<Dt.OldMachineUserInfo>> getMachineUsers(int pageNum, int pageSize) throws JsonProcessingException {
        // 1. call the new api
        Dt.W<Dt.Page<Dt.UserInfo2>> w = basicController.getUsers();
        // 2. convert the new api's result to the old api's result
        List<Dt.OldMachineUserInfo> l = new ArrayList<Dt.OldMachineUserInfo>();
        for (Dt.UserInfo2 i : w.data.voList) {
            Dt.OldMachineUserInfo o = Dt.OldMachineUserInfo.oldFromNew(i);
            l.add(o);
        }

        // 3. 🦜 : for backward compatibility, we need to provide paging.
        List<Dt.OldMachineUserInfo> l2 = pageList(l, pageNum, pageSize);

        // 4. return
        return new Dt.W<Dt.Page<Dt.OldMachineUserInfo>>(0, new Dt.Page<Dt.OldMachineUserInfo>(l2, w.data.totalCount));
    }

    @GetMapping("/machine/getRunDetail")
    @Operation(summary = "获取机器运行详情")
    public Dt.W<Dt.OldRunDetail> getRunDetail()
        throws JsonProcessingException
    {
        /*

          🦜 : Got from `uptime -s` and `uptime -p`

          call
          rpc GetBootTime (Empty) returns (HelloReply) {} // 🦜 : Get a string of format "YYYY-MM-DD HH:MM:SS"
        */

        Dt.OldRunDetail o = new Dt.OldRunDetail();

        try {
            HelloReply r = c.stub.getBootTime(Empty.newBuilder().build());
            o.boot_time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(r.getMsg());
        } catch (StatusRuntimeException e) {
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            return new Dt.W<Dt.OldRunDetail>(1, null, e.getStatus().getDescription());
        } catch (java.text.ParseException e) {
            log.error(S.RED + "📕️ Parse failed: {}" + S.NOR, e.getMessage());
            return new Dt.W<Dt.OldRunDetail>(1, null, e.getMessage());
        }

        // Calendar cal = Calendar.getInstance();
        // cal.add(Calendar.DATE, -1);
        // o.boot_time = cal.getTime();

        return new Dt.W<Dt.OldRunDetail>(0, o);
    }


    @EventListener(ApplicationReadyEvent.class)
    public void doSomethingAfterStartup() throws NoSuchAlgorithmException {
        log.info("🐢 : Application started, syncing");

        // 1. put in some Dt.OldAppAuthFailed, Dt.OldImportantAppAuthFailed
        // Dt.OldImportantAppAuthRegisterWithMore, Dt.OldAppAuthRegister
        // --------------------------------------------------
        oldAppAuthFailedRepo.saveAll(Dt.OldAppAuthFailed.mockList(5));
        oldAppAuthRegisterRepo.saveAll(Dt.OldAppAuthRegister.mockList(4));
        oldImportantAppAuthFailedRepo.saveAll(Dt.OldImportantAppAuthFailed.mockList(2));
        oldImportantAppAuthRegisterRepo.saveAll(Dt.OldImportantAppAuthRegisterWithMore.mockList(7));
        oldBashHistoryDaoRepo.saveAll(Dt.OldBashHistoryDao.mockList(30));

    }

}

@Tag(name="开放网口管理", description="🌐️: 管理哪些端口可以开放，这应该是要动系统防火墙，不过前置现在还没实现...所以目前这组接口就是一个数据库的读写删")
/**
 * @brief OpenPortController
 *
 * 🦜 : This class is meant to control which ports are open to the public. But
 * currently it only gets/sets data from/to the database, and does not instruct
 * the agent to do anything...
 */
@RestController
@RequestMapping("/openPort")
class OpenPortController
    extends ControllerBase{

    @GetMapping("/hi")
    @Operation(summary = "测试用(ping 用), 返回hi")
    public String get() throws JsonProcessingException {
        return "hi";
    }

    @Autowired
    OpenPortRepo repo;


    @PostMapping("/add")
    @Operation(summary = "开放一个端口",  description = "这个会把端口信息存到数据库，`createTime`不用填")
    public Dt.Error add(@RequestBody Dt.OpenPort o) throws JsonProcessingException {
        log.info("🌐️ : adding open port : {}", o);
        o.setCreateTime(new Date());
        repo.save(o);
        log.info("🌐️ : adding open port done {}", o);
        return Common.ok();
    }

    // private class OpenPortPage extends Dt.Page<Dt.OpenPort> {} // needed for older swagger
    /**
     * @brief Get the Allowed Port Infos By Pages object
     * @param pageNum : page number starts from 1
     */
    @GetMapping("/get")
    @Operation(summary = "获取开放端口信息")
    public Dt.W<Dt.Page<Dt.OpenPort>> get(int pageNum,
                                          int pageSize,
                                          @RequestParam(required = false)
                                          @Parameter(description = "端口号, 若无，找全部",
                                                     // required = false,
                                                     examples = {@ExampleObject(name = "22", value = "22", description = "ssh端口"),}
                                                     )
                                          Long port,
                                          @RequestParam(required = false)
                                          @Parameter(description = "找某过期日期之后的（不包括）",
                                                     // required = false,
                                                     examples = {@ExampleObject(name = "2024-01-01 12:00:00",
                                                                                value = "2024-01-01 12:00:00", description = "查2024年之后才过期的"),}
                                                     )
                                          String expiryTime
                                          ) throws JsonProcessingException,
                                                   java.text.ParseException {

        if(pageNum < 1 || pageSize < 0){
            throw new IllegalArgumentException("pageNum or pageSize is illegal");
        }

        if (expiryTime != null ) {
            Date d = new Date();
            d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(expiryTime);
            if (port != null){
                Page<Dt.OpenPort> page = repo.findByPortAndExpiryTimeAfter(port, d, PageRequest.of(pageNum-1, pageSize));
                return new Dt.W<Dt.Page<Dt.OpenPort>>(0, new Dt.Page<Dt.OpenPort>(page.getContent(),page.getTotalElements()));
            }else{              // no port, just expiryTime
                Page<Dt.OpenPort> page = repo.findByExpiryTimeAfter(d, PageRequest.of(pageNum-1, pageSize));
                return new Dt.W<Dt.Page<Dt.OpenPort>>(0, new Dt.Page<Dt.OpenPort>(page.getContent(),page.getTotalElements()));
            }
        }else{
            if (port != null){  // yes port, no expiryTime
                Page<Dt.OpenPort> page = repo.findByPort(port, PageRequest.of(pageNum-1, pageSize));
                return new Dt.W<Dt.Page<Dt.OpenPort>>(0, new Dt.Page<Dt.OpenPort>(page.getContent(),page.getTotalElements()));
            }
        }

        return ControllerBase.getPage(repo, pageNum, pageSize);
    }


    @GetMapping("/rm")
    @Operation(summary = "关闭一个开发的端口")
    public Dt.Error del(@Parameter(description = "开放端口的id", required = true)
                        Long id) throws JsonProcessingException {
        log.info("🚮️ : deleting open port with id : {}", id);
        repo.deleteById(id);
        return Common.ok();
    }
} // class OpenPortController


interface SafeboxRecordRepo extends JpaRepository<Dt.SafeboxRecord, String> {}
/**
 * @brief Where all files in Private folder are stored.
 *
 * 🦜 : this in fact is just a temp repo, every time `sync()` is called, the
 * repo will be cleared and re-populated with data from the agent.
 */
interface SafeboxFileRepo extends JpaRepository<Dt.SafeboxFile, Long> {
    Page<Dt.SafeboxFile> findByUser(String user, Pageable pageable);
    // find last access time by user
    Dt.SafeboxFile findTopByUserOrderByLastAccessTimeDesc(String user);
    // count the number of files by user
    Long countByUser(String user);
    // find all files that contains fileName
    Page<Dt.SafeboxFile> findByUserAndFileNameContaining(String user, String fileName, Pageable pageable);
}
// --------------------------------------------------
@Tag(name="文件保险箱", description="🦜: *每个用户*都可以创建*一个*，其实就是个文件夹，放在这里的文件大概会安全吧。"+
     "🐢 那可不可以每个用户创建多个保险箱可不可以？ 🦜 :....感觉应该行，但得研究一下，现在这个功能是调用了一个现成的工具"+
     "`ecryptfs-setup-private`(由Ubuntu开发)。要想多个保险箱的话就得自己写工具了....")
@RestController
@RequestMapping("/safebox")
class SafeboxController extends ControllerThatSyncs {
    @Autowired
    SafeboxFileRepo fileRepo;

    @Autowired
    SafeboxRecordRepo repo;


    /**
     * @brief Create a new safebox
     */
    @Operation(summary = "新建保险箱",
               description = "这个会用传进来的用户名和密码登录并创建一个保险箱，"+
               "之前创建过的话不行。如果一切顺利话会在那个用户的`$HOME`下创建一个`Private/`文件夹。"+
               "(所以路径一般就是`$HOME/Private/`)。 这个文件夹会在用户登录时被挂载(`mount`)， 而在用户退出时被卸载(`umount`)。"
               )
    @PostMapping("/create")
    public Dt.Error createSafebox(@RequestBody Dt.SafeboxRecord o)
        throws InterruptedException, JsonProcessingException{

        if (repo.existsById(o.getUser())) {
            // return Common.bad("user already has safebox", HttpStatus.CONFLICT);
            return Common.bad("user already has safebox");
        }

        // json to pb
        PswdLogin b = PswdLogin.newBuilder().setUsr(o.getUser()).setPswd(o.getPasswd()).build();
        HelloReply r;
        try {
            log.info(S.GREEN + "📕️ Creating safebox for : {}" + S.NOR, o.getUser());
            r = c.stub.createSafebox(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            return Common.bad(msg);
        }
        // safe the SafeboxRecord to db
        repo.save(o);
        return Common.ok();
    }

    @Operation(summary = "获取保险箱信息",
               description = "这里获取的就是之前`/create`时传进来的信息。"
               )
    @GetMapping("/get")
    public
        Dt.W<Dt.Page<Dt.SafeboxWithMore>>
        getPage(int pageNum,int pageSize) throws JsonProcessingException {
        // return safeboxService.getSafeboxBasicInfosByPages(pageNum,pageSize);
        // if(pageNum < 1 || pageSize < 0){
        //     throw new IllegalArgumentException("pageNum or pageSize is illegal");
        // }

        if(pageNum < 1 || pageSize < 0){
            return new Dt.W<Dt.Page<Dt.SafeboxWithMore>>(0, null , "不合法的页码或页大小");
        }

        // //从数据库中查询
        // // --------------------------------------------------
        Page<Dt.SafeboxRecord> page = repo.findAll(PageRequest.of(pageNum-1, pageSize));

        // static public class SafeboxWithMore extends SafeboxRecord {
        //     public Date lastAccessTime;
        //     public Long numFiles;
        // }
        // Extend the SafeboxRecord with the number of files in the safebox
        List<Dt.SafeboxWithMore> l = new ArrayList<Dt.SafeboxWithMore>();
        for (Dt.SafeboxRecord i : page.getContent()) {
            Dt.SafeboxWithMore o = new Dt.SafeboxWithMore();
            o.setUser(i.getUser());
            o.setPasswd(i.getPasswd());
            Dt.SafeboxFile f = fileRepo.findTopByUserOrderByLastAccessTimeDesc( i.getUser());

            if (f != null){
                o.setLastAccessTime(f.getLastAccessTime());
            }else{
                o.setLastAccessTime(new Date());
            }

            o.setNumFiles(fileRepo.countByUser(i.getUser()));
            l.add(o);
        }

        // return ResponseEntity.status(HttpStatus.OK)
        //     .body(new Dt.Page<Dt.SafeboxRecord>(page.getContent(),page.getTotalElements()));
        return new Dt.W<Dt.Page<Dt.SafeboxWithMore>>(0, new Dt.Page<Dt.SafeboxWithMore>(l,page.getTotalElements()));
    }

    @GetMapping("/ls")
    @Operation(summary = "列出某用户保险箱中的文件")
    public Dt.W<Dt.Page<Dt.SafeboxFile>>
        ls(
           @Parameter(description = "用户名",
                      examples = {@ExampleObject(name = "aaa", value = "aaa", description = "用户aaa"),}
                      )
           String user,
           @Parameter(description = "页码（从1开始）",
                      examples = {@ExampleObject(name = "1", value = "1", description = "第一页"),}
                      )
           int pageNum,
           @Parameter(description = "每页多少条",
                      examples = {@ExampleObject(name = "2", value = "2", description = "每页2条"),}
                      )
           int pageSize,
           @Parameter(description = "按哪行排序", required = false, examples = {
                   @ExampleObject(name = "fileName", value = "fileName", description = "默认按文件名排序"),
                   @ExampleObject(name = "size", value = "size", description = "按文件大小排序"),
                   @ExampleObject(name = "lastModifyTime", value = "lastModifyTime", description = "按最后修改时间排序"),
                   @ExampleObject(name = "lastAccessTime", value = "lastAccessTime", description = "按最后访问时间排序"),
                   @ExampleObject(name = "permissions", value = "permissions", description = "按权限排序")
               })
           String sortBy,
           @Parameter(description = "是否升序", required = false)
           Boolean asc,
           @Parameter(description = "文件名 (若无，找全部)", required = false)
           String fileName
           ) throws JsonProcessingException {

        // if the cache is too old, sync, 🐢 : kinda lazy eval, ohhh, greener than a scheduled task
        this.syncIfTooOld();    // call the base class's method

        if(pageNum < 1 || pageSize < 0){
            throw new IllegalArgumentException("pageNum or pageSize is illegal");
        }

        final Set<String> sortBys = new HashSet<String>(Arrays.asList("fileName", "size", "lastModifyTime", "lastAccessTime", "permissions"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "fileName");

        if (fileName != null  && fileName.length() > 0){
            Page<Dt.SafeboxFile> page = fileRepo.findByUserAndFileNameContaining(user, fileName, pageable);
            return new Dt.W<Dt.Page<Dt.SafeboxFile>>(0, new Dt.Page<Dt.SafeboxFile>(page.getContent(),page.getTotalElements()));
        }

        /*
          if (sortBy == null || sortBy.isEmpty()) {
          sortBy = "fileName";
          } else if (!sortBys.contains(sortBy)){
          throw new IllegalArgumentException("sortBy is illegal, it can only be one of " + sortBys.toString());
          }
          // --------------------------------------------------
          // 🦜 : use the powerfull spring-data, so powerful
          Sort sort = Sort.by(sortBy);
          if (asc != null && !asc) {
          sort = sort.descending();
          }else{
          sort = sort.ascending();
          }
          Pageable pageable = PageRequest.of(pageNum-1, pageSize, sort);
        */                     // refactored ^v
        Page<Dt.SafeboxFile> page = fileRepo.findByUser(user, pageable);
        return new Dt.W<Dt.Page<Dt.SafeboxFile>>(0, new Dt.Page<Dt.SafeboxFile>(page.getContent(),page.getTotalElements()));
    }

    @Hidden                     // 🦜 : Feels useless..
    @GetMapping("/has")
    @Operation(summary = "检查用户是否有保险箱")
    public Boolean contains(
                            @Parameter(description = "用户名",
                                       examples = {@ExampleObject(name = "aaa", value = "aaa", description = "用户aaa"),}
                                       )
                            String user) throws Throwable {
        return repo.existsById(user);
    }

    /**
     * @brief Update the sql.
     *
     * 🦜 : This will clear the `fileRepo` and then for all user who has safebox
     * (stored in `repo`), it will get the files in the safebox and save them in `fileRepo`.
     */
    @Override
    public void sync(){
        fileRepo.deleteAll();
        repo.findAll().forEach(r -> {
                try {
                    log.info( S.GREEN + "⚙️ : syncing safebox contents for user : {}" + S.NOR, r.getUser());
                    this.syncOne(r);
                } catch (Throwable e) {
                    log.error(S.RED + "❌️ : error syncing safebox for user : {}" + S.NOR, r.getUser(), e);
                    // do nothing
                }
            });
    }

    public synchronized void syncOne(Dt.SafeboxRecord o) throws StatusRuntimeException {
        // Form the SafeboxFile from the DescSafeboxRpl
        PswdLogin b = PswdLogin.newBuilder().setUsr(o.getUser()).setPswd(o.getPasswd()).build();
        DescSafeboxRpl r = c.stub.descSafebox(b);

        for (SimpleFileInfo i : r.getFilesList()) {
            Dt.SafeboxFile f = new Dt.SafeboxFile();

            // java from pb
            f.setUser(o.getUser()); // user is from `o`

            f.setFileName(i.getFileName());
            f.setSize(i.getSize());
            f.setLastModifyTime(new2Old(LocalDateTime.parse(i.getLastModifyTime(), fmt)));
            f.setLastAccessTime(new2Old(LocalDateTime.parse(i.getLastAccessTime(), fmt)));
            f.setPermissions(i.getPermissions());

            fileRepo.save(f);
        }
    }
}

interface ChangePswdRecordRepo extends JpaRepository<Dt.ChangePswdRecord,Long> {}

@Tag(name="root用户密码管理", description="🦜: `root`用户的密码会不停的变，用这个服务来查看现在最新（以及以往）`root`的登录密码吧。")
@RestController
@RequestMapping("/rootPswd")
@EnableScheduling
class RootAccountController extends ControllerBase{
    @Autowired
    ChangePswdRecordRepo repo;

    @PostMapping("/set")
    @Operation(summary = "设置root用户的密码")
    public Dt.Error set(@RequestBody Dt.ChangePswdReq o)
        throws JsonProcessingException{

        Calendar ca = Calendar.getInstance(); // the date factory
        Date now = ca.getTime();

        // upcast to ChangePswdRecord
        Dt.ChangePswdRecord r = new Dt.ChangePswdRecord();

        r.setPswd(o.getPswd());
        r.setExpiryTime(o.getExpiryTime());
        r.setCreateTime(now);

        try {
            this.saveAndApply(r);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            return Common.bad(msg);
        }

        return Common.ok();
    }

    @GetMapping("/get")
    @Operation(summary = "获取root用户的密码记录")
    public Dt.W<Dt.Page<Dt.ChangePswdRecord>>
        get(int pageNum,int pageSize) throws JsonProcessingException {
        // return rootAccountService.getRegisRecord(pageNum,pageSize);
        return ControllerBase.getPage(repo, pageNum, pageSize);
    }

    // --------------------------------------------------
    // one for whole class
    static Date noUpdateBefore = new Date(0); // 1970-01-01 00:00:00
    @Scheduled(cron = "0 0/10 * * * ?")
    @Transactional
    /**
     * @brief Try to change the root password to a random one
     *
     * 🐢 : This method is scheduled to run every 10 minutes. It generate a
     * Dt.ChangePswdRecord with `createTime` = now(), `expiryTime` = now() +
     * 9min30sec;
     *
     * 🦜 : Will this be run immediately?
     *
     * 🐢 : No, it will be run at the next 10 minute mark. (good for unit test)
     */
    public synchronized void changeRootPswdMaybe(){
        // 0. if it's time to change root password
        Calendar c = Calendar.getInstance(); // the date factory
        Date now = c.getTime();

        if (now.before(noUpdateBefore)) {
            log.info("🐸 : not time to change root password yet");
            return;
        }

        try {
            // 1. generate a Dt.ChangePswdRecord
            String nwPasswd = PasswdUtil.generateSecurePassword(16);
            Dt.ChangePswdRecord r = new Dt.ChangePswdRecord();
            r.setPswd(nwPasswd);
            r.setCreateTime(now);
            r.setExpiryTime(new Date(now.getTime() + (9*60 + 30)*1000)); // 9min30sec latter
            this.saveAndApply(r);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
        }
    }

    /**
     * @brief save the record and set the `noUpdateBefore` to the expiry time
     * @param r the record to save and apply
     */
    void saveAndApply(Dt.ChangePswdRecord r) throws StatusRuntimeException{
        if (r.getExpiryTime().before(new Date())) {
            log.error("⚠️ :? trying to apply old ChangePswdRecord");
            return;
        }
        repo.save(r);
        noUpdateBefore = r.getExpiryTime();
        this.askAgentToChangePswd(r.getPswd());
    }

    void askAgentToChangePswd(String pswd) throws StatusRuntimeException{
        // return rootAccountService.askAgentToChangePswd(o);
        HelloRequest b = HelloRequest.newBuilder().setName(pswd).build();
        HelloReply r;
        log.info(S.GREEN + "📕️ Asking agent to change pswd to : {}" + S.NOR, pswd);
        r = c.stub.changeRootPswd(b);
        log.info(S.GREEN + "📕️ Agent replied : {}" + S.NOR, r.getMsg());
    }

    static class PasswdUtil {
        private static final String LOWERCASE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz";
        private static final String UPPERCASE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        private static final String NUMERIC_CHARACTERS = "0123456789";
        //    private static final String SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|;:,.<>/?";

        private static final String ALL_CHARACTERS = LOWERCASE_CHARACTERS + UPPERCASE_CHARACTERS + NUMERIC_CHARACTERS;

        public static String generateSecurePassword(int length) {
            StringBuilder password = new StringBuilder();

            Random random = new SecureRandom();

            // Ensure at least one character from each character set
            password.append(LOWERCASE_CHARACTERS.charAt(random.nextInt(LOWERCASE_CHARACTERS.length())));
            password.append(UPPERCASE_CHARACTERS.charAt(random.nextInt(UPPERCASE_CHARACTERS.length())));
            password.append(NUMERIC_CHARACTERS.charAt(random.nextInt(NUMERIC_CHARACTERS.length())));

            // Fill the rest of the password
            for (int i = 3; i < length; i++) {
                int randomIndex = random.nextInt(ALL_CHARACTERS.length());
                password.append(ALL_CHARACTERS.charAt(randomIndex));
            }

            // Shuffle the characters in the password for better randomness
            char[] passwordArray = password.toString().toCharArray();
            for (int i = passwordArray.length - 1; i > 0; i--) {
                int index = random.nextInt(i + 1);
                char temp = passwordArray[index];
                passwordArray[index] = passwordArray[i];
                passwordArray[i] = temp;
            }

            return new String(passwordArray);
        }
    }
}

interface DeviceRecordRepo extends JpaRepository<Dt.DeviceRecord,Long>, JpaSpecificationExecutor<Dt.DeviceRecord> {}
interface IllegalDeviceRecord extends JpaRepository<Dt.IllegalDeviceRecord,Long>,
                                      JpaSpecificationExecutor<Dt.IllegalDeviceRecord> {}

@Tag(name="设备注册", description="🦜: 用这个服务来注册设备吧。")
@RestController
@RequestMapping("/device")
class DeviceController extends ControllerBase
{

    @Autowired
    IllegalDeviceRecord illegalDeviceRecord;

    // @GetMapping("/getIllegalDevice")
    // @Operation(summary = "获取非法接入记录")

    @Autowired
    DeviceRecordRepo repo;

    /*

      @Data
      @MappedSuperclass
      static public class DeviceRecordIn {
      String name;
      String code;
      String type;
      String auth;
      Date expiryTime;
      }

      @Entity
      @Data
      @EqualsAndHashCode(callSuper = false) // if base class is @Data, ignore warning from lombok
      static public class DeviceRecord extends DeviceRecordIn {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      public Long id;
      public Date createTime;
      }
    */

    @PostMapping("/regis")
    @Operation(summary = "注册设备")
    public Dt.Error regis(@RequestBody Dt.DeviceRecordIn o) throws JsonProcessingException{

        // make sub class
        Dt.DeviceRecord r = new Dt.DeviceRecord();
        r.setName(o.getName());
        r.setCode(o.getCode());
        r.setType(o.getType());
        r.setAuth(o.getAuth());
        r.setExpiryTime(o.getExpiryTime());
        r.setCreateTime(new Date());

        repo.save(r);

        return Common.ok();
    }

    @GetMapping("/unregis")
    @Operation(summary = "注销设备")
    public Dt.Error unregis(Long id) throws JsonProcessingException{

        if (id == null) {
            return Common.bad("id is null");
        }

        repo.deleteById(id);
        return Common.ok();
    }

    @GetMapping("/getRegistedDevice")
    @Operation(summary = "获取已注册设备")
    public Dt.W<Dt.Page<Dt.DeviceRecord>>
        getRegistedPage(int pageNum,int pageSize,
                        @RequestParam(required = false)
                        Long id,
                        @RequestParam(required = false)
                        String name,
                        @RequestParam(required = false)
                        String type,
                        @RequestParam(required = false)
                        @Parameter(description = "某个创建时间以后", required = false,
                                   examples = {@ExampleObject(name = "2021-01-01 00:00:00" ,
                                                              value = "2021-01-01 00:00:00" , description = "创建时间"),}
                                   )
                        String createTime,
                        @RequestParam(required = false)
                        @Parameter(description = "权限", required = false)
                        String auth
                        ) throws JsonProcessingException,
                                 java.text.ParseException {

        if (id != null) {
            Optional<Dt.DeviceRecord> o = repo.findById(id);
            if (o.isPresent()) {
                return new Dt.W<Dt.Page<Dt.DeviceRecord>>(0, new Dt.Page<Dt.DeviceRecord>(Arrays.asList(o.get()), 1L));
            } else {
                return new Dt.W<Dt.Page<Dt.DeviceRecord>>(0, new Dt.Page<Dt.DeviceRecord>(new ArrayList<Dt.DeviceRecord>(), 0L));
            }
        }

        // 1. prepare pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "name", "code", "type", "auth", "expiryTime", "createTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // 2. prepare spec
        Specification<Dt.DeviceRecord> spec = Specification.where(null);
        if (name != null && !name.isEmpty()) {
            spec = spec.and(Dt.DeviceRecord_.   nameContains(name));
        }
        if (type != null && !type.isEmpty()) {
            spec = spec.and(Dt.DeviceRecord_.typeEquals(type));
        }
        if (auth != null && !auth.isEmpty()) {
            spec = spec.and(Dt.DeviceRecord_.authEquals(auth));
        }
        if (createTime != null && !createTime.isEmpty()) {
            // parse from YYYY-MM-DD HH:MM:SS
            Date d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(createTime);
            spec = spec.and(Dt.DeviceRecord_.createTimeAfter(d));
        }

        // 3. find and return
        Page<Dt.DeviceRecord> page = repo.findAll(spec, pageable);
        return new Dt.W<Dt.Page<Dt.DeviceRecord>>(0, new Dt.Page<Dt.DeviceRecord>(page.getContent(),page.getTotalElements()));

    }
} // class DeviceController



interface BashHistoryEntryRepo extends JpaRepository<Dt.BashHistoryEntry2, Long>, JpaSpecificationExecutor<Dt.BashHistoryEntry2>
{
    Long countByTimeBetween(String since, String until);
}
@Tag(name="命令历史记录", description="🦜: 用这个服务来查看命令历史记录吧。")
@RestController
@RequestMapping("/cmdHistory")
class CmdHistoryController extends ControllerThatSyncs {
    // rpc GetBootTime (Empty) returns (HelloReply) {}

    @GetMapping("/get")
    @Operation(summary = "获取用户命令历史")
    public Dt.W<Dt.Page<Dt.BashHistoryEntry2>>
        get(@RequestParam(defaultValue = "1")
            @Parameter(description = "页码（从1开始）",
                       examples = {@ExampleObject(name = "1", value = "1", description = "第一页"),}
                       )
            int pageNum,
            @RequestParam(defaultValue = "2")
            @Parameter(description = "每页多少条", examples = {@ExampleObject(name = "2", value = "2", description = "每页2条"),}) int pageSize,
            @RequestParam(required = false)
            @Parameter(description = "ID", examples = {@ExampleObject(name = "1", value = "1", description = "查找ID=1的记录"),})
            Long id,
            @RequestParam(required = false)
            @Parameter(description = "用户名", examples = {@ExampleObject(name = "aaa", value = "aaa", description = "查找用户aaa的记录"),})
            String user,
            @RequestParam(required = false)
            @Parameter(description = "按哪行排序", required = false, examples = {@ExampleObject(name = "time", value = "time", description = "默认按时间排序"),})
            String sortBy,
            @RequestParam(required = false) String startTime,
            @RequestParam(required = false) String endTime,
            @RequestParam(required = false)
            @Parameter(description = "是否升序", required = false)
            Boolean asc
            ) throws JsonProcessingException {

        if (id != null) {
            Optional<Dt.BashHistoryEntry2> o = bashHistoryEntryRepo.findById(id);
            if (o.isPresent()) {
                return new Dt.W<Dt.Page<Dt.BashHistoryEntry2>>(0, new Dt.Page<Dt.BashHistoryEntry2>(new ArrayList<Dt.BashHistoryEntry2>(Arrays.asList(o.get())), 1L));
                // a page of 1
            }else{
                throw new RuntimeException("id not found");
            }
        }

        this.syncIfTooOld();

        // find page
        // 1. prepare pageable
        // --------------------------------------------------
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "user", "time", "cmd"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "id");

        // 2. prepare spec
        // --------------------------------------------------
        Specification<Dt.BashHistoryEntry2> spec = Specification.where(null);
        if (user != null && !user.isEmpty()) {
            spec = spec.and(Dt.BashHistoryEntry2_.userContains(user));
        }

        if (startTime != null) {
            spec = spec.and(Dt.BashHistoryEntry2_.timeAfter(startTime));
        }

        if (endTime != null) {
            spec = spec.and(Dt.BashHistoryEntry2_.timeBefore(endTime));
        }

        // 3. find and return
        // --------------------------------------------------
        Page<Dt.BashHistoryEntry2> page = bashHistoryEntryRepo.findAll(spec, pageable);
        return new Dt.W<Dt.Page<Dt.BashHistoryEntry2>>(0, new Dt.Page<Dt.BashHistoryEntry2>(page.getContent(),page.getTotalElements()));
    }

    @Autowired
    BashHistoryEntryRepo bashHistoryEntryRepo;

    // 🦜 : First let's override the `sync()` method
    @Override
    public void sync(){
        // --------------------------------------------------
        // sync the cmd history
        bashHistoryEntryRepo.deleteAll();
        List<Dt.BashHistoryEntry2> l2 = this.askBashHistoryFromAgent();
        bashHistoryEntryRepo.saveAll(l2);
    }

    public List<Dt.BashHistoryEntry2> askBashHistoryFromAgent() {
        // 1. prepare the request
        Empty b = Empty.newBuilder().build();
        // 2. call the agent
        GetBashHistoryRpl r;

        try {
            r = c.stub.getBashHistory(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // 3. pb -> java
        List<Dt.BashHistoryEntry2> l = new ArrayList<Dt.BashHistoryEntry2>();
        for (BashHistoryEntry i : r.getEntriesList()) {
            l.add(Dt.BashHistoryEntry2.convertFromPb(i));
        }

        // 4. return
        return l;
    }

    @EventListener
    public void init(ApplicationReadyEvent e) throws NoSuchAlgorithmException {
        // 0. mock some CmdHistory [reseved for now, the /old/bashHistory/... is used instead]
        // --------------------------------------------------
        // cmdHistoryRepo.saveAll(Dt.CmdHistory.mockList(20));
        // 🦜 : Here we can ask the agent now, so no need to mock anything.
    }

} // class CmdHistoryController

interface GenericCounterRepo extends JpaRepository<Dt.GenericCounter, Long>, JpaSpecificationExecutor<Dt.GenericCounter> {
    Long countByNameAndCreateTimeBetween(String name, Date since, Date until);
}
@Component
class CounterService {
    @Autowired
    GenericCounterRepo repo;

    public void inc(String name) {
        repo.save(Dt.GenericCounter.create(name));
    }

    public synchronized Long get(String n, Date since, Date until) {
        return repo.countByNameAndCreateTimeBetween(n, since, until);
    }

    public synchronized Long getInNDaysFromNow(String n , int d){
        // 🦜 : get today's (d = 0), yesterday's (d = 1), the day before yesterday's (d = 2), ...
        DayInterval di = DayInterval.nDaysFromNow(d);
        return this.get(n, di.since,di.until);
    }
}

class DayInterval {
    Date since;
    Date until;
    public DayInterval(Date since, Date until) {
        this.since = since;
        this.until = until;
    }

    public String getSinceString(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(since);
    }

    public String getUntilString(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(until);
    }

    public static DayInterval nDaysFromNow(int d){
        Calendar c = Calendar.getInstance(); // now
        c.add(Calendar.DATE, -d);
        // set hh:mm:ss to 00:00:00
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE,0);
        c.set(Calendar.SECOND,0);

        Date since = c.getTime();
        c.add(Calendar.DATE, 1);
        Date until = c.getTime();

        return new DayInterval(since, until);
    }
}


interface LoginInfoRepo extends JpaRepository<Dt.LoginInfo2, Long> ,
                                JpaSpecificationExecutor<Dt.LoginInfo2> {
    Long countByLoginTimeBetween(Date since, Date until);
}

interface ApplicantRepo extends JpaRepository<Dt.Applicant, Long>, JpaSpecificationExecutor<Dt.Applicant> {
    Long countByCreateTimeBetween(Date since, Date until);
}

@Tag(name="基础管理", description="🦜: 基础系统管理功能，包括查看用户,登入登出时间, 电脑配置等。")
@RestController
@RequestMapping("/basic")
class  BasicController extends ControllerThatSyncs {

    @GetMapping("/getUsers")
    @Operation(summary = "获取所有用户")
    public Dt.W<Dt.Page<Dt.UserInfo2>> getUsers() throws JsonProcessingException {
        /*
          🦜 : There won't be many, so we don't bother sql|paging here
        */
        Empty b = Empty.newBuilder().build();
        GetUsersRpl r;
        try {
            r = c.stub.getUsers(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // pb -> java
        List<Dt.UserInfo2> users = new ArrayList<Dt.UserInfo2>();
        for (UserInfo u : r.getUsersList()) {
            users.add(Dt.UserInfo2.convertFromPb(u));
        }

        return new Dt.W<Dt.Page<Dt.UserInfo2>>(0, new Dt.Page<Dt.UserInfo2>(users, (long) r.getUsersCount()));
    }

    @Autowired
    ProcessRegisterFailureRepo processRegisterFailureRepo;
    @Autowired
    ProcessRegisterRepo processRegisterRepo;
    @Autowired
    ProcessInfo2Repo processInfo2Repo;
    @Autowired
    BashHistoryEntryRepo bashHistoryEntryRepo;
    @Autowired
    LoginInfoRepo loginInfoRepo;
    @Autowired
    ApplicantRepo applicantRepo;
    @Autowired
    SafeboxFileRepo fileRepo;
    @Autowired
    CounterService counterService;

    @GetMapping("/getManyCounters")
    @Operation(summary = "获取一堆计数器")
    public Dt.W<Dt.ManyCounters> getManyCounters() throws JsonProcessingException {
        Dt.ManyCounters ct = fillCounters();
        try {
            /* 🦜 : fill in the Daily records

               1. x1 is the process check counter
               2. y1 = BashHistory record in certain day (legal), y2 = illegal commands <- 0 (for now)
               3. z1 = legal login = count of loginInfo
                  z2 = illegal login = 0 (for now)
                  z3 = root applicant = count of applicantRepo
             */
            for (int d = 0; d < 7;d++){
                log.info("🦜 : {}filling daily record for {} days ago{}", S.BLUE , d, S.NOR);
                DayInterval di = DayInterval.nDaysFromNow(d);
                Dt.DailyRecord o = new Dt.DailyRecord();
                o.date = di.since;

                o.x1 = counterService.get(PsController.CounterKey, di.since, di.until);
                o.y1 = bashHistoryEntryRepo.countByTimeBetween(di.getSinceString(), di.getUntilString());
                o.y2 = 0L;

                o.z1 = loginInfoRepo.countByLoginTimeBetween(di.since, di.until);
                o.z2 = 0L;

                o.z3 = applicantRepo.countByCreateTimeBetween(di.since, di.until);

                // 🦜 : Add this record
                ct.dailyRecords.add(o);
            }

            return new Dt.W<Dt.ManyCounters>(0, ct);
        }catch(Exception e){
            log.error("📕️ error: {}", e);
            e.printStackTrace();
            throw e;
        }
        // ==================================================
        // 🦜 : Get the Daily record..


        // return new Dt.W<Dt.ManyCounters>(0, Dt.ManyCounters.next());
    }

    Dt.ManyCounters fillCounters(){
        log.info("🦜 : filling counters");
        Dt.ManyCounters ct = new Dt.ManyCounters();

        // x1 = x3 = process check counter
        // get today's 00:00 -> now
        ct.x1 = counterService.getInNDaysFromNow(PsController.CounterKey,0);
        ct.x3 = ct.x1;
        // x2 = count of must-app failure
        Specification<Dt.ProcessRegisterFailure> spec = Specification.where(null);
        spec = spec.and(Dt.ProcessRegisterBase_.typeEquals_f("must"));
        ct.x2 = processRegisterFailureRepo.count(spec);

        // --------------------------------------------------
        // y1 = count of registered app
        ct.y1 = processRegisterRepo.count();

        // y2 = count of ok-app failure
        spec = Specification.where(null);
        spec = spec.and(Dt.ProcessRegisterBase_.typeEquals_f("ok"));
        ct.y2 = processRegisterFailureRepo.count(spec);

        // --------------------------------------------------
        /*
          y3 nApply of network controller. 🦜 : We use NetworkControllers.nSets,
          which got incremented when setting inbound rules.
         */
        ct.y3 = NetworkController.nSets.get();
        ct.y4 = 0L;              // 🦜 : how many times we successfully blocked a
                                // network access. Can't get it yet...
        // y5 is the network monitored byte count..
        ct.y5 = this.getY5NetworkMonitoredByteCount();

        // --------------------------------------------------
        /*
          z1 = count of recorded bashCommand,
          z2 = illegal bashCommands = 0 (currently)
         */
        ct.z1 = bashHistoryEntryRepo.count();
        ct.z2 = 0L;

        // --------------------------------------------------
        /*
          a1 : count of recorded login
          a2 : illegal login = 0 (currently)
          a3 : root user applicant = count of applicantRepo
         */
        ct.a1 = loginInfoRepo.count();
        ct.a2 = 0L;
        ct.a3 = applicantRepo.count();

        // --------------------------------------------------
        /*
          b1 : count of safebox file
         */
        ct.b1 = fileRepo.count();
        return ct;
    }

    @Autowired
    NetworkController networkController;

    Long getY5NetworkMonitoredByteCount() {
        // 1. call cctv4
        Map<Integer, Dt.InterfaceStat> m = networkController.previousInterfaceStats;
        // 2. sum up the i.ioCounterStat.bytesRecv
        Long sum = 0L;
        for (Dt.InterfaceStat i : m.values()) {
            sum += i.ioCounterStat.bytesRecv;
        }
        return sum;
    }

    List<String> readOutput(InputStream inputStream) throws IOException {
        try (BufferedReader output = new BufferedReader(new InputStreamReader(inputStream))) {
            return output.lines()
                .collect(Collectors.toList());
        }
    }

    void prepareTeX() throws IOException {
        // 0.1 the connection stats. (call get from  networkController)
        Dt.W<Dt.Page<Dt.ConnectionStat>> w = networkController.get(1, 100);
        List<Dt.ConnectionStat> l = w.data.voList;
        Utils.substituteSpecialLines(l, new Dt.ConnectionStat()); //[on/off]
        //                                    ^^^ dummy object for getter of template


        // 0.2 the inbound rules
        Dt.W<List<Dt.InboundRule2>> rs = networkController.getInboundRules();
        List<Dt.InboundRule2> l2 = rs.data;
        Utils.substituteSpecialLines(l2, new Dt.InboundRule2()); //[on/off]

        // 0.3 get the top 5 processes on sorted by cpuPercent (from processInfo2Repo)
        psController.syncIfTooOld();

        Pageable pageable = PageRequest.of(0, 8, Sort.by("cpuPercent").descending());
        Page<Dt.ProcessInfo2> page = processInfo2Repo.findAll(pageable);
        List<Dt.ProcessInfo2> l3 = page.getContent();
        // convert
        List<Dt.ProcessInfo2.CpuTeXBar> l3b = Dt.ProcessInfo2.CpuTeXBar.make(l3);
        Utils.substituteSpecialLines(l3b, new Dt.ProcessInfo2.CpuTeXBar()); //[on/off]

        // 0.3b get the top 5 processes on sorted by memPercent (from processInfo2Repo)
        pageable = PageRequest.of(0, 8, Sort.by("memPercent").descending());
        page = processInfo2Repo.findAll(pageable);
        l3 = page.getContent();
        // convert
        List<Dt.ProcessInfo2.MemoryTeXBar> l3c = Dt.ProcessInfo2.MemoryTeXBar.make(l3);
        Utils.substituteSpecialLines(l3c, new Dt.ProcessInfo2.MemoryTeXBar()); //[on/off]

        // 0.3c by readBytes
        pageable = PageRequest.of(0, 8, Sort.by("readBytes").descending());
        page = processInfo2Repo.findAll(pageable);
        l3 = page.getContent();
        // convert
        List<Dt.ProcessInfo2.ReadTeXBar> l3d = Dt.ProcessInfo2.ReadTeXBar.make(l3);
        Utils.substituteSpecialLines(l3d, new Dt.ProcessInfo2.ReadTeXBar()); //[on/off]

        // 0.3d by writeBytes
        pageable = PageRequest.of(0, 8, Sort.by("writeBytes").descending());
        page = processInfo2Repo.findAll(pageable);
        l3 = page.getContent();
        // convert
        List<Dt.ProcessInfo2.WriteTeXBar> l3e = Dt.ProcessInfo2.WriteTeXBar.make(l3);
        Utils.substituteSpecialLines(l3e, new Dt.ProcessInfo2.WriteTeXBar()); //[on/off]


        // 0.4 output the process register record and process failure
        Dt.W<Dt.Page<Dt.ProcessRegister>> w4 = psController.getRegisters(1,100,
                                                                         null,null,null,null
                                                                         );
        List<Dt.ProcessRegister> l4 = w4.data.voList;
        Utils.substituteSpecialLines(l4, new Dt.ProcessRegister()); //[on/off]
        Dt.W<Dt.Page<Dt.ProcessRegisterFailure>> w5 = psController.getFailures(1,100,
                                                                               "must",null,null,null);
        List<Dt.ProcessRegisterFailure> l5 = w5.data.voList;
        Utils.substituteSpecialLines(l5, new Dt.ProcessRegisterFailure()); //[on/off]

        // 0.5 output the cpu/memory/disk stats (call `askResourceInfoFromAgent()`)
        Dt.GetResourceInfoRpl2 rpl = this.askResourceInfoFromAgent();
        List<Dt.TeXKvEntry> l6 = new ArrayList<Dt.TeXKvEntry>();
        l6.add(new Dt.TeXKvEntry("hostname", rpl.hostname));

        l6.add(new Dt.TeXKvEntry("numberOfCores", String.format("%d", rpl.cpu.size())));

        l6.add(new Dt.TeXKvEntry("vendorId", rpl.cpu.getFirst().vendorId));
        l6.add(new Dt.TeXKvEntry("modelName", rpl.cpu.getFirst().modelName));
        l6.add(new Dt.TeXKvEntry("model", rpl.cpu.getFirst().model));
        l6.add(new Dt.TeXKvEntry("cacheSize",
                                 String.format("%d", rpl.cpu.getFirst().cacheSize)));
        l6.add(new Dt.TeXKvEntry("cpuUsage", String.format("%.1f", rpl.cpu_percent)));
        l6.add(new Dt.TeXKvEntry("cpuUsageDeg", String.format("%.0f",
                                                              rpl.cpu_percent * 3.6 // * 360 / 100
                                                              )));

        // memory stats
        l6.add(new Dt.TeXKvEntry("memorySize",
                                 Dt.TeXKvEntry.humanizeBytes(rpl.memory_info.total)));
        l6.add(new Dt.TeXKvEntry("usedMemory",
                                    Dt.TeXKvEntry.humanizeBytes(rpl.memory_info.used)));
        l6.add(new Dt.TeXKvEntry("memoryUsage",
                                    String.format("%.1f", rpl.memory_info.usedPercent)));
        l6.add(new Dt.TeXKvEntry("memoryUsageDeg",
                                    String.format("%.0f", rpl.memory_info.usedPercent * 3.6)));

        // disk
        l6.add(new Dt.TeXKvEntry("diskSize",
                                 Dt.TeXKvEntry.humanizeBytes(rpl.disk.total)));
        l6.add(new Dt.TeXKvEntry("usedDisk",
                                 Dt.TeXKvEntry.humanizeBytes(rpl.disk.used)));
        l6.add(new Dt.TeXKvEntry("diskPath",
                                 rpl.disk.path));
        l6.add(new Dt.TeXKvEntry("fileSystemType", rpl.disk.fstype));
        l6.add(new Dt.TeXKvEntry("diskUsage",
                                 String.format("%.1f", rpl.disk.usedPercent)));
        l6.add(new Dt.TeXKvEntry("diskUsageDeg",
                                    String.format("%.0f", rpl.disk.usedPercent * 3.6)));

        // write kvs
        Utils.substituteSpecialLines(l6, new Dt.TeXKvEntry("dummy","dummy")); //[on/off]
    }

    @Autowired
    PsController psController;
    @GetMapping("/pdf")
    @Operation(summary = "获取可信服务器报告pdf")
    public ResponseEntity<Resource> pdf() throws IOException {
        log.info("🦜 invoking TeX");

        // 0. prepare the tex
        // --------------------------------------------------
        prepareTeX();

        // 1. invoke the tex
        // --------------------------------------------------
        ProcessBuilder pb = new ProcessBuilder("lualatex","-shell-escape","report.tex");
        try {
            Process p = pb.start();
            p.waitFor();

            List<String> output = readOutput(p.getInputStream());
            List<String> error = readOutput(p.getErrorStream());
            log.info("output: {}", output);
            log.info("error: {}", error); // 21
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 2. get the pdf
        byte[] bytes = Files.readAllBytes(Paths.get("report.pdf"));

        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"")
            .header(HttpHeaders.CONTENT_TYPE, "application/pdf")
            .body(new ByteArrayResource(bytes));
    }


    final static Set<String> disabledDeviceCode = new HashSet<String>();

    @PostMapping("/addApplicant")
    @Operation(summary = "添加申请人")
    public Dt.Error addApplicant(@RequestBody Dt.ApplicantIn o) throws JsonProcessingException{

        // make sub class
        Dt.Applicant r = new Dt.Applicant();
        r.setName(o.getName());
        r.setPhone(o.getPhone());
        r.setSfz(o.getSfz());
        r.setCode(o.getCode());
        r.setDepartment(o.getDepartment());
        r.setExpiryTime(o.getExpiryTime());
        r.setCreateTime(new Date());

        applicantRepo.save(r);

        return Common.ok();
    }

    @GetMapping("/rmApplicant")
    @Operation(summary = "删除申请人")
    public Dt.Error rmApplicant(
                                @RequestParam
                                @Parameter(description = "申请人的唯一标识符",
                                           examples = {@ExampleObject(name = "1" ,
                                                                      value = "1" , description = "申请人的唯一标识符"),}
                                           )
                                Long id) throws JsonProcessingException{
        applicantRepo.deleteById(id);
        return Common.ok();
    }

    @GetMapping("/getApplicants")
    @Operation(summary = "获取所有申请人")
    public Dt.W<Dt.Page<Dt.Applicant>>
        getApplicants(int pageNum, int pageSize,
                      @RequestParam(required = false)
                      Long id,
                      @RequestParam(required = false)
                      String name,
                      @RequestParam(required = false)
                      String phone,
                      @RequestParam(required = false)
                      String createTime,
                      @RequestParam(required = false)
                      Boolean expired) throws JsonProcessingException,
                                              java.text.ParseException
    {

        if (id != null) {
            Optional<Dt.Applicant> o = applicantRepo.findById(id);
            if (o.isPresent()) {
                return new Dt.W<Dt.Page<Dt.Applicant>>(0, new Dt.Page<Dt.Applicant>(Arrays.asList(o.get()), 1L));
            } else {
                return new Dt.W<Dt.Page<Dt.Applicant>>(0, new Dt.Page<Dt.Applicant>(new ArrayList<Dt.Applicant>(), 0L));
            }
        }

        // find page
        if (pageNum < 1 || pageSize < 0) {
            return new Dt.W<Dt.Page<Dt.Applicant>>(0, null, "不合法的页码或页大小");
        }

        // 1. prepare pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("id", "name", "phone", "sfz", "code", "department", "expiryTime", "createTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id");

        // 2. prepare spec
        Specification<Dt.Applicant> spec = Specification.where(null);
        if (name != null && !name.isEmpty()) {
            spec = spec.and(Dt.Applicant_.nameContains(name));
        }
        if (phone != null && !phone.isEmpty()) {
            spec = spec.and(Dt.Applicant_.phoneContains(phone));
        }
        if (createTime != null && !createTime.isEmpty()) {
            // parse from YYYY-MM-DD HH:MM:SS
            Date d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(createTime);
            spec = spec.and(Dt.Applicant_.createTimeAfter(d));
        }
        if (expired != null) {
            if (expired) {
                spec = spec.and(Dt.Applicant_.expiryTimeBefore(new Date()));
            } else {
                spec = spec.and(Dt.Applicant_.expiryTimeAfter(new Date()));
            }
        }

        log.info("🐸 : finding page with spec = {}", spec);
        // 3. find and return
        try{
            Page<Dt.Applicant> page = applicantRepo.findAll(spec, pageable);
            log.info("🐸 : found page = {}", page);
            return new Dt.W<Dt.Page<Dt.Applicant>>(0, new Dt.Page<Dt.Applicant>(page.getContent(),page.getTotalElements()));
        }
        catch(Exception e){
            log.error("📕️ error: {}", e);
            throw e;
        }
    }


    @GetMapping("/getDeviceInterfaces")
    @Operation(summary = "获取USB信息")
    public Dt.W<List<Dt.DeviceInterface>> getDeviceInterfaces() throws JsonProcessingException {
        /*
          🦜 : There won't be many, so we don't bother sql|paging here
        */
        List<Dt.DeviceInterface> l = new ArrayList<Dt.DeviceInterface>();
        l.add(new Dt.DeviceInterface("usb3", "TS21367812", new Date(), false));
        l.add(new Dt.DeviceInterface("usb2", "TS21367813", new Date(), false));
        l.add(new Dt.DeviceInterface("typec","TS21367814", new Date(), false));
        l.add(new Dt.DeviceInterface("usb3","TS21367815", new Date(), false));
        l.add(new Dt.DeviceInterface("usb2","TS21367816", new Date(), false));
        l.add(new Dt.DeviceInterface("typec","TS21367817", new Date(), false));

        for (Dt.DeviceInterface i : l) {
            if (disabledDeviceCode.contains(i.code)) {
                i.disabled = true;
            }
        }

        return new Dt.W<List<Dt.DeviceInterface>>(0, l);
    }

    @GetMapping("/toggleDevice")
    @Operation(summary = "禁用/启用usb设备")
    public Dt.Error toggleDevice(
                                 @RequestParam
                                 @Parameter(description = "设备的唯一标识符",
                                            examples = {@ExampleObject(name = "TS21367817" ,
                                                                       value = "TS21367817" , description = "设备的唯一标识符"),}
                                            )
                                 String code) throws JsonProcessingException
    {
        if (disabledDeviceCode.contains(code)) {
            disabledDeviceCode.remove(code);
        }else{
            disabledDeviceCode.add(code);
        }

        return Common.ok();
    }


    public List<Dt.LoginInfo2> askLoginInfoFromAgent(LocalDateTime sinceTime, LocalDateTime untilTime, String user) {
        // 1. prepare the request
        GetLoginHistoryReq.Builder bl = GetLoginHistoryReq.newBuilder()
            .setSinceTime(sinceTime.format(fmt))
            .setUntilTime(untilTime.format(fmt));
        if (user != null && !user.isEmpty()) {
            bl.setUser(user);
        }

        GetLoginHistoryReq b = bl.build();
        // 2. call the agent
        GetLoginHistoryRpl r;

        try {
            r = c.stub.getLoginHistory(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // 3. pb -> java
        List<Dt.LoginInfo2> l = new ArrayList<Dt.LoginInfo2>();
        for (LoginInfo i : r.getLoginInfosList()) {
            l.add(Dt.LoginInfo2.convertFromPb(i));
        }

        // 4. return
        return l;
    }


    // 🦜 : First let's override the `sync()` method
    @Override
    public void sync(){
        // 1. call the agent to get the list
        // --------------------------------------------------
        LocalDateTime untilTime = LocalDateTime.now();
        // a year ago
        LocalDateTime sinceTime = untilTime.minusYears(1);
        List<Dt.LoginInfo2> l = this.askLoginInfoFromAgent(sinceTime, untilTime, "");
        // ^^^ get all the login history in the past year

        // 2. update the repo
        // --------------------------------------------------
        loginInfoRepo.deleteAll();
        loginInfoRepo.saveAll(l);
    }

    @GetMapping("/cctv1")
    @Operation(summary = "获取系统资源信息", description = "🦜: 获取系统资源信息，包括CPU,内存,磁盘,IO等。一般用websocket去收听`/ws/cctv1`，这个会每个几秒传一个这个。")

    public Dt.W<Dt.GetResourceInfoRpl2> cctv1() throws JsonProcessingException {
        return new Dt.W<Dt.GetResourceInfoRpl2>(0, this.askResourceInfoFromAgent());
    }

    @Autowired
    ThreadPoolTaskScheduler t;
    @Autowired
    ObjectMapper json;

    @GetMapping("/getLoginHistory")
    @Operation(summary = "获取用户登录历史")
    public Dt.W<Dt.Page<Dt.LoginInfo2>>
        getLoginHistory(@RequestParam(defaultValue = "1")
                        @Parameter(description = "页码（从1开始）",
                                   examples = {@ExampleObject(name = "1", value = "1", description = "第一页"),}
                                   )
                        int pageNum,
                        @RequestParam(defaultValue = "2")
                        @Parameter(description = "每页多少条",
                                   examples = {@ExampleObject(name = "2", value = "2", description = "每页2条"),}
                                   )
                        int pageSize,
                        @RequestParam(required = false)
                        @Parameter(description = "用户名",
                                   examples = {
                                       @ExampleObject(name = "aaa", value = "aaa", description = "查找用户aaa的记录"),
                                       @ExampleObject(name = "", value = "", description = "查找所有用户的记录"),
                                   })
                        String user,
                        @RequestParam(required = false)
                        @Parameter(description = "某个时间以后登录的", required = false,
                                   examples = {@ExampleObject(name = "2021-01-01 00:00:00" ,
                                                              value = "2021-01-01 00:00:00" , description = "某个时间后登录的"),}
                                   )
                        String sinceTime,
                        @RequestParam(required = false)
                        @Parameter(description = "某个时间以前登录的", required = false,
                                   examples = {@ExampleObject(name = "2021-01-01 00:00:00" ,
                                                              value = "2021-01-01 00:00:00" , description = "某个时间前登录的"),}
                                   )
                        String untilTime,
                        @RequestParam(required = false)
                        @Parameter(description = "按哪行排序", required = false, examples = {
                                @ExampleObject(name = "loginTime", value = "loginTime", description = "默认按登录时间排序"),
                                @ExampleObject(name = "logoutTime", value = "logoutTime", description = "按登出时间排序"),
                            })
                        String sortBy,
                        @RequestParam(required = false)
                        @Parameter(description = "是否升序", required = false)
                        Boolean asc) throws JsonProcessingException {

        log.info("{}🐸 : getLoginHistory called with pageNum={}, pageSize={}, user={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
                 S.CYAN, pageNum, pageSize, user, sinceTime, untilTime, sortBy, asc, S.NOR);

        this.syncIfTooOld();

        // 1. prepare pageable
        final Set<String> sortBys = new HashSet<String>(Arrays.asList("loginTime", "logoutTime"));
        Pageable pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "loginTime");
        Page<Dt.LoginInfo2> page;

        // 2. prepare spec
        Specification<Dt.LoginInfo2> spec = Specification.where(null);
        if (user != null && !user.isEmpty()) {
            spec = spec.and(Dt.LoginInfo2_.userContains(user));
        }
        if (sinceTime != null && !sinceTime.isEmpty()) {
            LocalDateTime sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt);
            Date sinceTimeDate = new2Old(sinceTimeLocalDateTime);
            spec = spec.and(Dt.LoginInfo2_.loginTimeAfter(sinceTimeDate));
        }

        if (untilTime != null && !untilTime.isEmpty()) {
            LocalDateTime untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt);
            Date untilTimeDate = new2Old(untilTimeLocalDateTime);
            spec = spec.and(Dt.LoginInfo2_.loginTimeBefore(untilTimeDate));
        }

        // 3. find and return
        page = loginInfoRepo.findAll(spec, pageable);
        // log.info("{}🐸 : found page of {}", S.CYAN, page.getTotalElements() , S.NOR);
        return new Dt.W<Dt.Page<Dt.LoginInfo2>>(0, new Dt.Page<Dt.LoginInfo2>(page.getContent(),page.getTotalElements()));
    }

    @EventListener              // 🦜 : What's this ? 🐢 : This is the spring magic to run something on startup.
    public void init(ApplicationReadyEvent e) throws NoSuchAlgorithmException {
        // System.out.println("⚙️ Initializing  BasicController");

        // 0.1 mock some Applicants
        // --------------------------------------------------
        applicantRepo.saveAll(Dt.Applicant.mockList(11));


        // 1. schedule the task
        // --------------------------------------------------
        t.scheduleAtFixedRate(
                              () -> {
                                  try {
                                      Dt.GetResourceInfoRpl2 r = this.askResourceInfoFromAgent();
                                      Wc1.say(json.writeValueAsString(r));
                                  } catch (Exception ex) {
                                      log.error("🐸 : error saying cctv4", ex);
                                  }
                              },
                              Duration.of(5, ChronoUnit.SECONDS)
                              ); // every two seconds, boardcast to all listener of /hi

        // 2. try prepare the TeX
        // --------------------------------------------------
        try {
            prepareTeX();
        }catch (Exception ex) {
            log.error("🐸 : error generating pdf", ex);
            ex.printStackTrace();
        }
        log.info( S.BLUE + "🐸 📗️: TeX prepared" +S.NOR);
    }

    // --------------------------------------------------
    public Dt.GetResourceInfoRpl2 askResourceInfoFromAgent(){
        Empty b = Empty.newBuilder().build();
        GetResourceInfoRpl r;

        // rpc GetResourceInfo (Empty) returns (GetResourceInfoRpl) {}
        try {
            r = c.stub.getResourceInfo(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // --------------------------------------------------
        // 2. pb -> Java
        return Dt.GetResourceInfoRpl2.convertFromPb(r);
    }

} // class BasicController

// class NetworkController
interface ConnectionStatsRepo extends JpaRepository<Dt.ConnectionStat, Long> {}
@Tag(name="网络", description="🦜: 这个可以看网络信息，包括哪些口是被开的，还有连接啥的。")
@RestController
@RequestMapping("/net")
class NetworkController extends ControllerThatSyncs {

    public Map<Integer, Dt.InterfaceStat> previousInterfaceStats = new HashMap<Integer, Dt.InterfaceStat>();

    @Autowired
    ThreadPoolTaskScheduler t;
    @Autowired
    ObjectMapper json;

    // --------------------------------------------------
    // cctv4 functionality

    @EventListener              // 🦜 : What's this ? 🐢 : This is the spring magic to run something on startup.
    public void init(ApplicationReadyEvent e) {
        System.out.println("⚙️ Initializing NetworkController tasks");

        t.scheduleAtFixedRate(
                              () -> {
                                  try {
                                      Map<Integer, Dt.InterfaceStat> m = askInterfaceInfoFromAgent();
                                      List<Dt.InterfaceStat> l = new ArrayList<Dt.InterfaceStat>(m.values());
                                      Wc4.say(json.writeValueAsString(l));
                                  } catch (Exception ex) {
                                      log.error("🐸 : error saying cctv4", ex);
                                  }
                              },
                              Duration.of(5, ChronoUnit.SECONDS)
                              ); // every two seconds, boardcast to all listener of /hi
    }

    @GetMapping("/cctv4")
    @Operation(summary = "获取网络接口信息", description = "🦜: 获取网络接口信息，包括每个口的ip,mac,流量等。一般用websocket去收听`/ws/cctv4`，这个会每个几秒传一个这个。")
    public Dt.W<List<Dt.InterfaceStat>> cctv4() throws JsonProcessingException {
        Map<Integer, Dt.InterfaceStat> m = this.askInterfaceInfoFromAgent();
        return new Dt.W<List<Dt.InterfaceStat>>(0, new ArrayList<Dt.InterfaceStat>(m.values()));
    }


    static LocalDateTime lastTimeWeGotNIC = LocalDateTime.now().minusDays(1);

    public synchronized Map<Integer, Dt.InterfaceStat> askInterfaceInfoFromAgent(){

        Empty b = Empty.newBuilder().build();
        GetNICInfoRpl r;

        // 1. call the agent
        // rpc GetNICInfo (Empty) returns (GetNICInfoRpl) {}
        try {
            r = c.stub.getNICInfo(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // 2. pb -> Java
        Map<Integer, Dt.InterfaceStat> m = new HashMap<Integer, Dt.InterfaceStat>();
        for (InterfaceStat i : r.getInterfaceStatsList()) {
            Dt.InterfaceStat i2 = convertFromPb(i);

            // --------------------------------------------------
            // 🦜 :If we have lastTimeWeGotNICiousInterfaceStats, we can calculate the speed
            if (previousInterfaceStats.containsKey(i2.getIndex())) {
                Dt.InterfaceStat i3 = previousInterfaceStats.get(i2.getIndex());
                // 🦜 : calculate the speed using `LocalDateTime lastSyncTime`
                long secSinceLastSync = Duration.between(lastTimeWeGotNIC, LocalDateTime.now()).getSeconds();
                // 🦜 : calculate the speed using `IOCounterStat ioCounterStat`
                i2.ioCounterStat.recvSpeedBytePerSec = (i2.ioCounterStat.bytesRecv - i3.ioCounterStat.bytesRecv) / secSinceLastSync;
                i2.ioCounterStat.sentSpeedBytePerSec = (i2.ioCounterStat.bytesSent - i3.ioCounterStat.bytesSent) / secSinceLastSync;
            }else{
                // set the speed to 0
                i2.ioCounterStat.recvSpeedBytePerSec = 0L;
                i2.ioCounterStat.sentSpeedBytePerSec = 0L;

            }

            m.put(i2.getIndex(), i2);
        }

        // 🦜 : save the current interface stats and update time
        previousInterfaceStats = m;
        lastTimeWeGotNIC = LocalDateTime.now();

        return m;
    }

    private static Dt.InterfaceStat convertFromPb(InterfaceStat i){
        Dt.InterfaceStat i2 = new Dt.InterfaceStat();
        i2.setIndex(i.getIndex());
        i2.setMtu(i.getMtu());
        i2.setName(i.getName());
        i2.setHardwareAddr(i.getHardwareAddr());
        i2.setAddrs(i.getAddrsList());
        i2.setFlags(i.getFlagsList());
        i2.setDefaultGateway(i.getDefaultGateway());

        Dt.IOCounterStat ioCounterStat = new Dt.IOCounterStat();
        ioCounterStat.setBytesSent(i.getIoCounterStat().getBytesSent());
        ioCounterStat.setBytesRecv(i.getIoCounterStat().getBytesRecv());
        ioCounterStat.setPacketsSent(i.getIoCounterStat().getPacketsSent());
        ioCounterStat.setPacketsRecv(i.getIoCounterStat().getPacketsRecv());
        ioCounterStat.setErrin(i.getIoCounterStat().getErrin());
        ioCounterStat.setErrout(i.getIoCounterStat().getErrout());
        ioCounterStat.setDropin(i.getIoCounterStat().getDropin());
        ioCounterStat.setDropout(i.getIoCounterStat().getDropout());
        i2.setIoCounterStat(ioCounterStat);

        return i2;
    }

    // --------------------------------------------------
    // 🦜 : netstats functionality 
    @Autowired
    ConnectionStatsRepo repo;
    Set<Integer> closedPids = new HashSet<Integer>();
    @GetMapping("/get")
    @Operation(summary = "获取网络连接信息")
    public Dt.W<Dt.Page<Dt.ConnectionStat>> get(int pageNum,int pageSize) throws JsonProcessingException {
        this.syncIfTooOld();
        Dt.W<Dt.Page<Dt.ConnectionStat>> p =  ControllerBase.getPage(repo, pageNum, pageSize);
        // change the closedPids
        for (Dt.ConnectionStat i : p.data.voList) {
            // if (closedPids.contains(i.getPid())) {
            //     i.setClosed(true);
            // }else{
            //     i.setClosed(false);
            // }
            i.setClosed(closedPids.contains(i.getPid()));
        }
        return p;
    }


    @GetMapping("/toggle")
    @Operation(summary = "开关网络口")
    public Dt.Error toggle(
                           @Parameter(description = "网络口的pid",
                                      examples = {@ExampleObject(name = "123", value = "123", description = "pid=123的口"),}
                                      )
                           Integer pid) throws JsonProcessingException {
        if (pid == null) {
            return Common.bad("pid is null");
        }

        if (closedPids.contains(pid)) {
            closedPids.remove(pid);
        }else{
            closedPids.add(pid);
        }
        return Common.ok();

    }

    @Override
    public void sync(){
        repo.deleteAll();
        // 🦜 : Call agent's   rpc GetNetstatsInfo (Empty) returns (GetNetstatsInfoRpl) {}
        Empty b = Empty.newBuilder().build();
        GetNetstatsInfoRpl r;
        try {
            r = c.stub.getNetstatsInfo(b);
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            throw new RuntimeException(msg);
        }

        // 🦜 : pb -> java and save
        for (ConnectionStat i : r.getConnectionStatsList()) {
            Dt.ConnectionStat i2 = convertFromPb(i);
            repo.save(i2);
        }
    }

    private static Dt.ConnectionStat convertFromPb(ConnectionStat i){
        Dt.ConnectionStat i2 = new Dt.ConnectionStat();

        i2.setFd(i.getFd());
        i2.setFamily(i.getFamily());
        i2.setType(i.getType());

        Dt.AddrPort localAddr = new Dt.AddrPort();
        localAddr.setAddr(i.getLocalAddr().getAddr());
        localAddr.setPort(i.getLocalAddr().getPort());

        i2.setLocalAddr(localAddr); // <-> 1,2,connect
        // localAddr.setConnectionStat(i2);

        Dt.AddrPort remoteAddr = new Dt.AddrPort();
        remoteAddr.setAddr(i.getRemoteAddr().getAddr());
        remoteAddr.setPort(i.getRemoteAddr().getPort());

        i2.setRemoteAddr(remoteAddr); // <-> 1,2,connect
        // remoteAddr.setConnectionStat(i2);

        i2.setStatus(i.getStatus());
        i2.setUids(i.getUidsList());
        i2.setPid(i.getPid());
        i2.setExeName(i.getExeName());

        i2.setClosed(true);
        return i2;
    }

    // --------------------------------------------------
    // firewall functionality
    /*

      public static class InboundRule2{
      String type;            // tcp | udp | icmp | all
      String portRange;
      String source;
      Boolean ipv6;
    */
    List<Dt.InboundRule2> inboundRules = new ArrayList<Dt.InboundRule2>();

    public static AtomicLong nSets = new AtomicLong(1); // 🦜 : how many times the `/SetInboundRules` is called.

    @GetMapping("/getInboundRules")
    @Operation(summary = "获取入站规则")
    public Dt.W<List<Dt.InboundRule2>> getInboundRules() throws JsonProcessingException {
        return new Dt.W<List<Dt.InboundRule2>>(0, inboundRules);
    }

    @PostMapping("/setInboundRules")
    @Operation(summary = "设置入站规则",
               description = "🦜: 设置入站规则. `type`可以是`tcp`, `udp`, `icmp`, `all`。`portRange`是一个范围，比如`80:90` 也可以是单个如`80`。"+
               "`source`是一个或多个ip地址如`1.2.3.4`,`1.2.3.4/24`, `0.0.0.0/0`,`fd00::/8`。")
    public Dt.Error setInboundRules(@RequestBody List<Dt.InboundRule2> inboundRules) throws JsonProcessingException {
        nSets.incrementAndGet(); // 🦜 : increments the counter

        this.inboundRules = inboundRules;

        /*
          🦜 call the agent: rpc SetInboundRules (SetInboundRulesReq) returns (HelloReply) {} // 🦜 : return "ok" 
          message SetInboundRulesReq {
          repeated InboundRule rules = 1;
          }
          1. prepare the req
        */
        SetInboundRulesReq.Builder b = SetInboundRulesReq.newBuilder();
        for (Dt.InboundRule2 i : inboundRules) {
            b.addRules(i.convertToPb());
        }

        // 2. call the agent
        try {
            HelloReply r = c.stub.setInboundRules(b.build());
            log.info("🐸 : setInboundRules, agent answered: {}", r.getMsg());
        } catch (StatusRuntimeException e) {
            String msg =  e.getStatus().getDescription();
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.getStatus());
            return Common.bad(msg);
        }

        return Common.ok();
    }

} // class NetworkController


// ^^^ controllers
// --------------------------------------------------
class S{
    public static final String RED = "\u001B[31m";
    public static final String GREEN = "\u001B[32m";
    public static final String YELLOW  = "\u001B[33m";
    public static final String BLUE  = "\u001B[34m";
    public static final String MAGENTA  = "\u001B[35m";
    public static final String CYAN  = "\u001B[36m";
    public static final String NOR  = "\u001B[0m";
}

// common stuff
class Common implements AutoCloseable {
    public static Logger log = LoggerFactory.getLogger(Common.class);
    public ObjectMapper json;
    ManagedChannel ch;
    GreeterGrpc.GreeterBlockingStub stub;

    public Common(String agentUrl) {
        // this.ch = Grpc.newChannelBuilder("localhost:50051", InsecureChannelCredentials.create()).build();
        this.ch = Grpc.newChannelBuilder(agentUrl, InsecureChannelCredentials.create()).build();
        this.stub = GreeterGrpc.newBlockingStub(ch);

        this.json = mapper();
    }

    public static Dt.Error ok() throws JsonProcessingException{
        // return json.writeValueAsString(Dt.Error.SUCCESS);
        return Dt.Error.SUCCESS;
    }


    public static Dt.Error bad(String msg, HttpStatus status)throws JsonProcessingException{
        return new Dt.Error(-1,msg);
    }

    public static Dt.Error bad(String msg)throws JsonProcessingException{
        // return json.writeValueAsString(new Dt.Error(-1,msg));
        // return ResponseEntity.status(HttpStatus.BAD_REQUEST)
        //     .body(new Dt.Error(-1,msg));
        // return bad(msg, HttpStatus.BAD_REQUEST);
        return bad(msg, HttpStatus.OK); //<! 🦜 : required by colleagues
    }

    public static ObjectMapper mapper(){
        ObjectMapper m = new ObjectMapper();

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        m.setDateFormat(df);
        return m;
    }

    // dtor (finalizer)
    @Override
    public void close() {
        try {
            ch.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
        }catch (InterruptedException e) {
            log.error("❌️ : error closing grpc channel: ", e);
        }
    }
}

@OpenAPIDefinition(
                   info = @Info(
                                title = "这是可信服务器（受控服务器）管理系统的API",
                                version = "1.2",
                                description = "⚠️: 请注意所有时间戳的格式都是`yyyy-MM-dd hh:mm:ss`，"+
                                "但是因为作者最终还是没能搞懂swagger的对象反映射魔法，所以在这个swagger自动生成的"+
                                "文档里示例的时间戳格式一般会是一串像`2023-12-25T09:36:22.847Z`的东西，"+
                                "请在发送和接受参数的时候使用 `yyyy-MM-dd hh:mm:ss` 格式。"
                                )
                   )
@SpringBootApplication
@EnableWebSocket
public class M implements WebSocketConfigurer {

    static Logger log = LoggerFactory.getLogger(M.class);

    /*
      🦜 : bean for repo is automatically created.
    */
    @Autowired
    public OpenPortRepo openPortRepo;
    @Autowired
    public SafeboxRecordRepo safeboxRecordRepo;
    @Autowired
    public ChangePswdRecordRepo changePswdRecordRepo;
    @Autowired
    public SafeboxFileRepo safeboxFileRepo;


    public static void main(String[] args) {
        SpringApplication.run(M.class, args);
    }

    @Value("${machine.agent-url}")
    public String agentUrl;


    @Bean
    public Common common(){
        // --------------------------------------------------
        /*

          🦜 : Here we start the scheduled tasks (in fact there're many places
          we can do this in, but let's just do it here)
        */
        // t.scheduleAtFixedRate(
        //                       () -> {
        //                           try {
        //                               // Wc1.say();
        //                               // Wc4.say();

        //                               // 🦜 : say them

        //                               Dt.GetResourceInfoRpl2 r = basicController.askResourceInfoFromAgent();
        //                               Wc1.say(json.writeValueAsString(r));

        //                           } catch (Exception e) {
        //                               log.error("🐸 : error saying cctv1", e);
        //                           }
        //                       },
        //                       Duration.of(5, ChronoUnit.SECONDS)
        //                       ); // every two seconds, boardcast to all listener of /hi

        // --------------------------------------------------
        return new Common(agentUrl);
    }

    @Autowired
    ThreadPoolTaskScheduler t;

    @Autowired
    Wc1 wc1;                    // 🦜 : why they're here? 🐢 : Brought in for register
    @Autowired
    Wc4 wc4;


    /*
      🦜 : Kinda kave to do it this way, so we ensure that there's only one wc1 and wc4
    */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
        registry.addHandler(wc1, "/ws/cctv1").setAllowedOrigins("*");
        registry.addHandler(wc4, "/ws/cctv4").setAllowedOrigins("*");
    }
}

@Configuration
class C1{
    @Bean
    public Wc1 wc1(){return new Wc1();}
    @Bean
    public Wc4 wc4(){return new Wc4();}
    /*
      🦜 : By declaring a primary bean of ObjectMapper, we change how spring-boot parse and serialize json
    */
    @Primary
    @Bean
    public ObjectMapper json(){return Common.mapper();}

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler t = new ThreadPoolTaskScheduler();
        t.setPoolSize(5);
        t.setThreadNamePrefix("ThreadPoolTaskScheduler");
        return t;
    }
}

abstract class Wc extends TextWebSocketHandler{
    static Logger log = LoggerFactory.getLogger(Wc.class);

    static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketSession>> all = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws IOException {
        log.info("🌱 established: " + session);
        // session.sendMessage(new TextMessage("🌐️ 欢迎收看: " + channel()));
        // add to listener
        if (all.get(channel()) == null) {
            all.put(channel(), new CopyOnWriteArraySet<>());
        }
        all.get(channel()).add(session);
    }


    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws IOException,
                                                                                           RuntimeException
    {
        log.info("🚮️ closed: " + session);
        // remove from listener
        if (all.get(channel()) != null) {
            all.get(channel()).remove(session);
        }else{
            // ?? fatal, unregisted channel?
            throw new RuntimeException("fatal, unregisted channel?");
        }
    }

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException{
        log.info("🐸 : [ignored]: audience said " + message.getPayload());
        // session.sendMessage(new TextMessage("Hi from server"));
    }

    abstract String channel();

    public static void boardcast(String k, String msg){
        String mmsg = msg;
        // cap to 30 char is msg is too long
        if (msg.length() > 30) {
            msg = msg.substring(0, 30) + "...";
        }

        log.info("🐸 : boardcast() to " + k + ": " + S.CYAN + msg + S.NOR);
        if (all.get(k) != null) {
            for (WebSocketSession s : all.get(k)) {
                try {
                    s.sendMessage(new TextMessage(mmsg));
                } catch (Exception e) {
                    log.error("🐸 : error sending msg: ", e);
                }
            }
        }

    }
} // class Wc

class Wc1 extends Wc {
    @Override
    String channel(){return "/cctv1";}
    public static void say(){
        Wc.boardcast("/cctv1", "CCTV1: " + LocalDateTime.now().toString());
    }
    public static void say(String s){
        Wc.boardcast("/cctv1", s);
    }
}

class Wc4 extends Wc {
    @Override
    String channel(){return "/cctv4";}
    static String[] words = {"🐸", "🐸🐸", "🐸🐸🐸", "🐸🐸🐸🐸", "🐸🐸🐸🐸🐸"};
    static int i = 0;
    public static void say(){
        Wc.boardcast("/cctv4", "CCTV4: " + words[i++ % words.length]);
    }
    public static void say(String s){
        Wc.boardcast("/cctv4", s);
    }
}

// /*
//   🦜 : Here, the globle exception handler is defined
// */
@ControllerAdvice
class GlobalExceptionHandler {

    // @ExceptionHandler(Exception.class)
    @ExceptionHandler(RuntimeException.class)
    public Dt.Error handleException(Exception e) {
        // Customize your response here
        // return new ResponseEntity<>("An error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        return new Dt.Error(-1, e.getMessage());
    }
}
